1use cssparser::*;
2use std::rc::Rc;
3use taffy::Size;
4
5use crate::{
6 Viewport,
7 error::StyleSheetParseError,
8 style::{CalcArena, FromCss, LengthDefaultsToZero, SizingContext},
9};
10
11#[derive(Debug, Clone, PartialEq)]
12enum MediaType {
13 All,
14 Screen,
15 Unsupported(String),
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19enum MediaFeatureComparison {
20 Equal,
21 Min,
22 Max,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26enum MediaOrientation {
27 Portrait,
28 Landscape,
29}
30
31#[derive(Debug, Clone, PartialEq)]
32enum MediaFeature {
33 Width(MediaFeatureComparison, LengthDefaultsToZero),
34 Height(MediaFeatureComparison, LengthDefaultsToZero),
35 Orientation(MediaOrientation),
36}
37
38#[derive(Debug, Clone, PartialEq)]
39struct MediaQuery {
40 media_type: MediaType,
41 features: Vec<MediaFeature>,
42 negated: bool,
43}
44
45#[derive(Debug, Clone, PartialEq, Default)]
46pub struct MediaQueryList {
47 queries: Vec<MediaQuery>,
48}
49
50impl MediaFeature {
51 fn matches(&self, viewport: Viewport, sizing: &SizingContext) -> bool {
52 match self {
53 Self::Width(comparison, value) => viewport.size.width.is_some_and(|width| {
54 compare_media_feature(*comparison, width as f32, value.to_px(sizing, width as f32))
55 }),
56 Self::Height(comparison, value) => viewport.size.height.is_some_and(|height| {
57 compare_media_feature(
58 *comparison,
59 height as f32,
60 value.to_px(sizing, height as f32),
61 )
62 }),
63 Self::Orientation(MediaOrientation::Portrait) => viewport
64 .size
65 .width
66 .zip(viewport.size.height)
67 .is_some_and(|(width, height)| height >= width),
68 Self::Orientation(MediaOrientation::Landscape) => viewport
69 .size
70 .width
71 .zip(viewport.size.height)
72 .is_some_and(|(width, height)| width > height),
73 }
74 }
75}
76
77impl MediaQuery {
78 fn matches(&self, viewport: Viewport, sizing: &SizingContext) -> bool {
79 let media_type_matches = match &self.media_type {
80 MediaType::All | MediaType::Screen => true,
81 MediaType::Unsupported(_) => false,
82 };
83
84 let mut is_match = media_type_matches
85 && self
86 .features
87 .iter()
88 .all(|feature| feature.matches(viewport, sizing));
89
90 if self.negated {
91 is_match = !is_match;
92 }
93
94 is_match
95 }
96}
97
98impl MediaQueryList {
99 pub(crate) fn parse<'i, 't>(
100 input: &mut Parser<'i, 't>,
101 ) -> Result<Self, ParseError<'i, StyleSheetParseError>> {
102 Ok(Self {
103 queries: input.parse_comma_separated(parse_media_query)?,
104 })
105 }
106
107 pub fn matches(&self, viewport: Viewport) -> bool {
108 if self.queries.is_empty() {
109 return true;
110 }
111
112 let sizing = SizingContext {
113 viewport,
114 container_size: Size::NONE,
115 font_size: viewport.font_size,
116 root_font_size: None,
117 line_height: viewport.font_size,
118 root_line_height: Some(viewport.font_size),
119 calc_arena: Rc::new(CalcArena::default()),
120 };
121
122 self
123 .queries
124 .iter()
125 .any(|query| query.matches(viewport, &sizing))
126 }
127}
128
129fn compare_media_feature(comparison: MediaFeatureComparison, actual: f32, expected: f32) -> bool {
130 const MEDIA_FEATURE_EQUALITY_TOLERANCE: f32 = 0.5;
131
132 match comparison {
133 MediaFeatureComparison::Equal => (actual - expected).abs() <= MEDIA_FEATURE_EQUALITY_TOLERANCE,
134 MediaFeatureComparison::Min => actual >= expected,
135 MediaFeatureComparison::Max => actual <= expected,
136 }
137}
138
139fn parse_media_query<'i, 't>(
140 input: &mut Parser<'i, 't>,
141) -> Result<MediaQuery, ParseError<'i, StyleSheetParseError>> {
142 let mut negated = false;
143 let mut media_type = MediaType::All;
144 let mut features = Vec::new();
145 let mut has_explicit_media_type = false;
146
147 if let Ok(keyword) = input.try_parse(Parser::expect_ident_cloned) {
148 if keyword.eq_ignore_ascii_case("not") {
149 negated = true;
150 } else if !keyword.eq_ignore_ascii_case("only") {
151 media_type = parse_media_type(keyword);
152 has_explicit_media_type = true;
153 }
154
155 if !has_explicit_media_type && let Ok(name) = input.try_parse(Parser::expect_ident_cloned) {
157 media_type = parse_media_type(name);
158 has_explicit_media_type = true;
159 }
160 }
161
162 if input
163 .try_parse(|input| parse_media_feature_block(input, &mut features))
164 .is_ok()
165 || has_explicit_media_type
166 {
167 while input
168 .try_parse(|input| input.expect_ident_matching("and"))
169 .is_ok()
170 {
171 parse_media_feature_block(input, &mut features)?;
172 }
173 }
174
175 Ok(MediaQuery {
176 media_type,
177 features,
178 negated,
179 })
180}
181
182fn parse_media_type(name: CowRcStr<'_>) -> MediaType {
183 if name.eq_ignore_ascii_case("all") {
184 MediaType::All
185 } else if name.eq_ignore_ascii_case("screen") {
186 MediaType::Screen
187 } else {
188 MediaType::Unsupported(name.to_string())
189 }
190}
191
192fn parse_media_feature_block<'i, 't>(
193 input: &mut Parser<'i, 't>,
194 features: &mut Vec<MediaFeature>,
195) -> Result<(), ParseError<'i, StyleSheetParseError>> {
196 let location = input.current_source_location();
197 let token = input.next()?;
198 match token {
199 Token::ParenthesisBlock => input.parse_nested_block(|input| {
200 features.push(parse_media_feature(input)?);
201 Ok(())
202 }),
203 _ => Err(location.new_unexpected_token_error(token.clone())),
204 }
205}
206
207fn parse_media_feature<'i, 't>(
208 input: &mut Parser<'i, 't>,
209) -> Result<MediaFeature, ParseError<'i, StyleSheetParseError>> {
210 let feature_name = input.expect_ident_cloned()?;
211 input.expect_colon()?;
212
213 if feature_name.eq_ignore_ascii_case("orientation") {
214 let orientation = input.expect_ident_cloned()?;
215 return if orientation.eq_ignore_ascii_case("portrait") {
216 Ok(MediaFeature::Orientation(MediaOrientation::Portrait))
217 } else if orientation.eq_ignore_ascii_case("landscape") {
218 Ok(MediaFeature::Orientation(MediaOrientation::Landscape))
219 } else {
220 Err(
221 input.new_error(BasicParseErrorKind::UnexpectedToken(Token::Ident(
222 orientation,
223 ))),
224 )
225 };
226 }
227
228 let comparison = if feature_name.eq_ignore_ascii_case("min-width")
229 || feature_name.eq_ignore_ascii_case("min-height")
230 {
231 MediaFeatureComparison::Min
232 } else if feature_name.eq_ignore_ascii_case("max-width")
233 || feature_name.eq_ignore_ascii_case("max-height")
234 {
235 MediaFeatureComparison::Max
236 } else {
237 MediaFeatureComparison::Equal
238 };
239
240 let length = LengthDefaultsToZero::from_css(input).map_err(ParseError::into)?;
241
242 if feature_name.eq_ignore_ascii_case("width")
243 || feature_name.eq_ignore_ascii_case("min-width")
244 || feature_name.eq_ignore_ascii_case("max-width")
245 {
246 Ok(MediaFeature::Width(comparison, length))
247 } else if feature_name.eq_ignore_ascii_case("height")
248 || feature_name.eq_ignore_ascii_case("min-height")
249 || feature_name.eq_ignore_ascii_case("max-height")
250 {
251 Ok(MediaFeature::Height(comparison, length))
252 } else {
253 Err(input.new_custom_error(StyleSheetParseError::unsupported_media_feature()))
254 }
255}