1use std::fmt;
2
3use crate::style::{ToCss, properties::write_css_string, unexpected_token};
4use cssparser::{Parser, Token, match_ignore_ascii_case};
5
6use crate::style::{
7 CssDescriptorKind, CssSyntaxKind, CssToken, FromCss, Length, MakeComputed, ParseResult, Sides,
8 SizingContext, SpacePair,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Default)]
15pub enum FillRule {
16 #[default]
18 NonZero,
19 EvenOdd,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Default)]
25pub enum ShapeRadius {
26 #[default]
28 ClosestSide,
29 FarthestSide,
31 Length(Length),
33}
34
35impl MakeComputed for ShapeRadius {
36 fn make_computed(&mut self, sizing: &SizingContext) {
37 if let ShapeRadius::Length(length) = self {
38 length.make_computed(sizing);
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45#[non_exhaustive]
46pub struct ShapePosition(pub SpacePair<Length>);
47
48impl MakeComputed for ShapePosition {
49 fn make_computed(&mut self, sizing: &SizingContext) {
50 self.0.make_computed(sizing);
51 }
52}
53
54impl Default for ShapePosition {
55 fn default() -> Self {
56 Self(SpacePair::from_single(Length::Percentage(50.0)))
57 }
58}
59
60#[derive(Debug, Clone, PartialEq)]
65#[non_exhaustive]
66pub struct InsetShape {
67 pub inset: Sides<Length>,
69 pub border_radius: Option<Sides<Length>>,
71}
72
73impl MakeComputed for InsetShape {
74 fn make_computed(&mut self, sizing: &SizingContext) {
75 self.inset.make_computed(sizing);
76 self.border_radius.make_computed(sizing);
77 }
78}
79
80#[derive(Debug, Clone, PartialEq)]
82#[non_exhaustive]
83pub struct EllipseShape {
84 pub radius_x: ShapeRadius,
86 pub radius_y: ShapeRadius,
88 pub position: ShapePosition,
90}
91
92impl MakeComputed for EllipseShape {
93 fn make_computed(&mut self, sizing: &SizingContext) {
94 self.radius_x.make_computed(sizing);
95 self.radius_y.make_computed(sizing);
96 self.position.make_computed(sizing);
97 }
98}
99
100pub type PolygonCoordinate = SpacePair<Length>;
102
103#[derive(Debug, Clone, PartialEq)]
105#[non_exhaustive]
106pub struct PolygonShape {
107 pub fill_rule: Option<FillRule>,
109 pub coordinates: Box<[PolygonCoordinate]>,
111}
112
113impl MakeComputed for PolygonShape {
114 fn make_computed(&mut self, sizing: &SizingContext) {
115 self.coordinates.make_computed(sizing);
116 }
117}
118
119#[derive(Debug, Clone, PartialEq)]
121#[non_exhaustive]
122pub struct PathShape {
123 pub fill_rule: Option<FillRule>,
125 pub path: Box<str>,
127}
128
129#[derive(Debug, Clone, PartialEq)]
131pub enum BasicShape {
132 Inset(Box<InsetShape>),
134 Ellipse(Box<EllipseShape>),
136 Polygon(PolygonShape),
138 Path(PathShape),
140}
141
142impl MakeComputed for BasicShape {
143 fn make_computed(&mut self, sizing: &SizingContext) {
144 match self {
145 BasicShape::Inset(shape) => shape.make_computed(sizing),
146 BasicShape::Ellipse(shape) => shape.make_computed(sizing),
147 BasicShape::Polygon(shape) => shape.make_computed(sizing),
148 BasicShape::Path(_) => {}
149 }
150 }
151}
152
153impl BasicShape {
154 pub fn fill_rule(&self) -> Option<FillRule> {
155 match self {
156 BasicShape::Polygon(shape) => shape.fill_rule,
157 BasicShape::Path(shape) => shape.fill_rule,
158 _ => None,
159 }
160 }
161}
162
163crate::style::properties::declare_enum_from_css_impl!(
164 FillRule,
165 "nonzero" => FillRule::NonZero,
166 "evenodd" => FillRule::EvenOdd,
167);
168
169impl<'i> FromCss<'i> for ShapeRadius {
170 fn from_css(parser: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
171 let location = parser.current_source_location();
172
173 if let Ok(length) = parser.try_parse(Length::from_css) {
175 return Ok(ShapeRadius::Length(length));
176 }
177
178 let ident = parser.expect_ident()?;
180 match_ignore_ascii_case! { &ident,
181 "closest-side" => Ok(ShapeRadius::ClosestSide),
182 "farthest-side" => Ok(ShapeRadius::FarthestSide),
183 _ => Err(unexpected_token!(location, &Token::Ident(ident.clone()))),
184 }
185 }
186
187 const VALID_TOKENS: &'static [CssToken] = &[
188 CssToken::Keyword("closest-side"),
189 CssToken::Keyword("farthest-side"),
190 CssToken::Syntax(CssSyntaxKind::Length),
191 ];
192}
193
194impl<'i> FromCss<'i> for ShapePosition {
195 fn from_css(parser: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
196 let first = Length::from_css(parser)?;
197
198 let second = parser
200 .try_parse(Length::from_css)
201 .unwrap_or(Length::Percentage(50.0));
202
203 Ok(ShapePosition(SpacePair::from_pair(first, second)))
204 }
205
206 const VALID_TOKENS: &'static [CssToken] = Length::<true>::VALID_TOKENS;
207}
208
209impl<'i> FromCss<'i> for BasicShape {
210 fn from_css(parser: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
211 let location = parser.current_source_location();
212 let token = parser.next()?;
213
214 match token {
215 Token::Function(function) => {
216 match_ignore_ascii_case! { &function,
217 "inset" => parser.parse_nested_block(|input| {
218 let inset = Sides::from_css(input)?;
219
220 let border_radius = if input.try_parse(|input| input.expect_ident_matching("round")).is_ok() {
222 Some(Sides::from_css(input)?)
223 } else {
224 None
225 };
226
227 Ok(BasicShape::Inset(Box::new(InsetShape {
228 inset,
229 border_radius,
230 })))
231 }),
232 "circle" => parser.parse_nested_block(|input| {
233 let radius = input.try_parse(ShapeRadius::from_css).unwrap_or_default();
234
235 let position = if input.try_parse(|input| input.expect_ident_matching("at")).is_ok() {
236 ShapePosition::from_css(input)?
237 } else {
238 ShapePosition::default()
239 };
240
241 Ok(BasicShape::Ellipse(Box::new(EllipseShape { radius_x: radius, radius_y: radius, position })))
242 }),
243 "ellipse" => parser.parse_nested_block(|input| {
244 let radius_x = ShapeRadius::from_css(input)?;
245 let radius_y = input.try_parse(ShapeRadius::from_css).unwrap_or_default();
246
247 let position = if input.try_parse(|input| input.expect_ident_matching("at")).is_ok() {
248 ShapePosition::from_css(input)?
249 } else {
250 ShapePosition::default()
251 };
252
253 Ok(BasicShape::Ellipse(Box::new(EllipseShape { radius_x, radius_y, position })))
254 }),
255 "polygon" => parser.parse_nested_block(|input| {
256 let fill_rule = input.try_parse(FillRule::from_css).ok();
257 if fill_rule.is_some() {
258 input.expect_comma()?;
259 }
260
261 Ok(BasicShape::Polygon(PolygonShape {
262 fill_rule,
263 coordinates: input
264 .parse_comma_separated(PolygonCoordinate::from_css)?
265 .into_boxed_slice(),
266 }))
267 }),
268 "path" => parser.parse_nested_block(|input| {
269 let fill_rule = input.try_parse(FillRule::from_css).ok();
270 if fill_rule.is_some() {
271 input.expect_comma()?;
272 }
273
274 let path = input.expect_string()?.as_ref().into();
275
276 Ok(BasicShape::Path(PathShape {
277 fill_rule,
278 path,
279 }))
280 }),
281 _ => Err(unexpected_token!(location, token)),
282 }
283 }
284 _ => Err(unexpected_token!(location, token)),
285 }
286 }
287
288 const VALID_TOKENS: &'static [CssToken] = &[
289 CssToken::Descriptor(CssDescriptorKind::InsetFn),
290 CssToken::Descriptor(CssDescriptorKind::CircleFn),
291 CssToken::Descriptor(CssDescriptorKind::EllipseFn),
292 CssToken::Descriptor(CssDescriptorKind::PolygonFn),
293 CssToken::Descriptor(CssDescriptorKind::PathFn),
294 ];
295}
296
297impl ToCss for ShapeRadius {
298 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
299 match self {
300 Self::ClosestSide => dest.write_str("closest-side"),
301 Self::FarthestSide => dest.write_str("farthest-side"),
302 Self::Length(l) => l.to_css(dest),
303 }
304 }
305}
306
307impl ToCss for ShapePosition {
308 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
309 self.0.to_css(dest)
310 }
311}
312
313impl ToCss for BasicShape {
314 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
315 match self {
316 Self::Inset(shape) => {
317 dest.write_str("inset(")?;
318 shape.inset.to_css(dest)?;
319 if let Some(radius) = &shape.border_radius {
320 dest.write_str(" round ")?;
321 radius.to_css(dest)?;
322 }
323 dest.write_char(')')
324 }
325 Self::Ellipse(shape) => {
326 if shape.radius_x == shape.radius_y {
327 dest.write_str("circle(")?;
328 let mut has_radius = false;
329 if shape.radius_x != ShapeRadius::ClosestSide {
330 shape.radius_x.to_css(dest)?;
331 has_radius = true;
332 }
333 if shape.position != ShapePosition::default() {
334 if has_radius {
335 dest.write_char(' ')?;
336 }
337 dest.write_str("at ")?;
338 shape.position.to_css(dest)?;
339 }
340 dest.write_char(')')
341 } else {
342 dest.write_str("ellipse(")?;
343 shape.radius_x.to_css(dest)?;
344 dest.write_char(' ')?;
345 shape.radius_y.to_css(dest)?;
346 if shape.position != ShapePosition::default() {
347 dest.write_str(" at ")?;
348 shape.position.to_css(dest)?;
349 }
350 dest.write_char(')')
351 }
352 }
353 Self::Polygon(shape) => {
354 dest.write_str("polygon(")?;
355 if let Some(rule) = shape.fill_rule {
356 rule.to_css(dest)?;
357 dest.write_str(", ")?;
358 }
359 let mut first = true;
360 for coord in shape.coordinates.iter() {
361 if !first {
362 dest.write_str(", ")?;
363 }
364 coord.to_css(dest)?;
365 first = false;
366 }
367 dest.write_char(')')
368 }
369 Self::Path(shape) => {
370 dest.write_str("path(")?;
371 if let Some(rule) = shape.fill_rule {
372 rule.to_css(dest)?;
373 dest.write_str(", ")?;
374 }
375 write_css_string(dest, &shape.path)?;
376 dest.write_char(')')
377 }
378 }
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use std::assert_matches;
385
386 use super::*;
387 use Length::*;
388
389 #[test]
390 fn test_parse_inset_simple() {
391 assert_eq!(
392 BasicShape::from_str("inset(10px)"),
393 Ok(BasicShape::Inset(Box::new(InsetShape {
394 inset: Sides([Px(10.0); 4]),
395 border_radius: None,
396 })))
397 );
398 }
399
400 #[test]
401 fn test_parse_inset_four_values() {
402 assert_eq!(
403 BasicShape::from_str("inset(10px 20px 30px 40px)"),
404 Ok(BasicShape::Inset(Box::new(InsetShape {
405 inset: Sides([Px(10.0), Px(20.0), Px(30.0), Px(40.0)]),
406 border_radius: None,
407 })))
408 );
409 }
410
411 #[test]
412 fn test_parse_inset_with_border_radius() {
413 assert_eq!(
414 BasicShape::from_str("inset(10px round 5px)"),
415 Ok(BasicShape::Inset(Box::new(InsetShape {
416 inset: Sides::from(Px(10.0)),
417 border_radius: Some(Sides::from(Px(5.0))),
418 })))
419 );
420 }
421
422 #[test]
423 fn test_parse_inset_with_complex_border_radius() {
424 assert_eq!(
425 BasicShape::from_str("inset(10px 20px 30px 40px round 5px 10px 15px 20px)"),
426 Ok(BasicShape::Inset(Box::new(InsetShape {
427 inset: Sides([Px(10.0), Px(20.0), Px(30.0), Px(40.0)]),
428 border_radius: Some(Sides([Px(5.0), Px(10.0), Px(15.0), Px(20.0)])),
429 })))
430 );
431 }
432
433 #[test]
434 fn test_parse_circle_simple() {
435 assert_eq!(
436 BasicShape::from_str("circle(50px)"),
437 Ok(BasicShape::Ellipse(Box::new(EllipseShape {
438 radius_x: ShapeRadius::Length(Px(50.0)),
439 radius_y: ShapeRadius::Length(Px(50.0)),
440 position: ShapePosition::default(),
441 })))
442 );
443 }
444
445 #[test]
446 fn test_parse_circle_with_position() {
447 assert_eq!(
448 BasicShape::from_str("circle(50px at 25% 75%)"),
449 Ok(BasicShape::Ellipse(Box::new(EllipseShape {
450 radius_x: ShapeRadius::Length(Px(50.0)),
451 radius_y: ShapeRadius::Length(Px(50.0)),
452 position: ShapePosition(SpacePair {
453 x: Length::Percentage(25.0),
454 y: Length::Percentage(75.0),
455 }),
456 })))
457 );
458 }
459
460 #[test]
461 fn test_parse_circle_default_radius() {
462 assert_eq!(
463 BasicShape::from_str("circle(at 25% 75%)"),
464 Ok(BasicShape::Ellipse(Box::new(EllipseShape {
465 radius_x: ShapeRadius::ClosestSide,
466 radius_y: ShapeRadius::ClosestSide,
467 position: ShapePosition(SpacePair {
468 x: Length::Percentage(25.0),
469 y: Length::Percentage(75.0),
470 }),
471 })))
472 );
473 }
474
475 #[test]
476 fn test_parse_ellipse_simple() {
477 assert_eq!(
478 BasicShape::from_str("ellipse(50px 30px)"),
479 Ok(BasicShape::Ellipse(Box::new(EllipseShape {
480 radius_x: ShapeRadius::Length(Px(50.0)),
481 radius_y: ShapeRadius::Length(Px(30.0)),
482 position: ShapePosition::default(),
483 })))
484 );
485 }
486
487 #[test]
488 fn test_parse_ellipse_with_position() {
489 assert_eq!(
490 BasicShape::from_str("ellipse(50px 30px at 25% 75%)"),
491 Ok(BasicShape::Ellipse(Box::new(EllipseShape {
492 radius_x: ShapeRadius::Length(Px(50.0)),
493 radius_y: ShapeRadius::Length(Px(30.0)),
494 position: ShapePosition(SpacePair {
495 x: Length::Percentage(25.0),
496 y: Length::Percentage(75.0),
497 }),
498 })))
499 );
500 }
501
502 #[test]
503 fn test_parse_polygon_triangle() {
504 assert_matches!(
505 BasicShape::from_str("polygon(50% 0%, 0% 100%, 100% 100%)"),
506 Ok(BasicShape::Polygon(PolygonShape {
507 fill_rule: None,
508 coordinates: coords,
509 })) if coords.len() == 3 &&
510 coords[0] == SpacePair { x: Length::Percentage(50.0), y: Length::Percentage(0.0) } &&
511 coords[1] == SpacePair { x: Length::Percentage(0.0), y: Length::Percentage(100.0) } &&
512 coords[2] == SpacePair { x: Length::Percentage(100.0), y: Length::Percentage(100.0) }
513 );
514 }
515
516 #[test]
517 fn test_parse_polygon_with_fill_rule() {
518 assert_matches!(
519 BasicShape::from_str("polygon(evenodd, 50% 0%, 0% 100%, 100% 100%)"),
520 Ok(BasicShape::Polygon(PolygonShape {
521 fill_rule: Some(FillRule::EvenOdd),
522 coordinates: coords,
523 })) if coords.len() == 3
524 );
525 }
526
527 #[test]
528 fn test_parse_path() {
529 assert_eq!(
530 BasicShape::from_str("path('M 10 10 L 90 90')"),
531 Ok(BasicShape::Path(PathShape {
532 fill_rule: None,
533 path: "M 10 10 L 90 90".into(),
534 }))
535 );
536 }
537
538 #[test]
539 fn test_parse_path_with_fill_rule() {
540 assert_eq!(
541 BasicShape::from_str("path(evenodd, 'M 10 10 L 90 90')"),
542 Ok(BasicShape::Path(PathShape {
543 fill_rule: Some(FillRule::EvenOdd),
544 path: "M 10 10 L 90 90".into(),
545 }))
546 );
547 }
548
549 #[test]
550 fn test_parse_circle_percentage_radius() {
551 assert_eq!(
552 BasicShape::from_str("circle(50%)"),
553 Ok(BasicShape::Ellipse(Box::new(EllipseShape {
554 radius_x: ShapeRadius::Length(Length::Percentage(50.0)),
555 radius_y: ShapeRadius::Length(Length::Percentage(50.0)),
556 position: ShapePosition::default(),
557 })))
558 );
559 }
560
561 #[test]
562 fn test_parse_circle_closest_side() {
563 assert_eq!(
564 BasicShape::from_str("circle(closest-side)"),
565 Ok(BasicShape::Ellipse(Box::new(EllipseShape {
566 radius_x: ShapeRadius::ClosestSide,
567 radius_y: ShapeRadius::ClosestSide,
568 position: ShapePosition::default(),
569 })))
570 );
571 }
572
573 #[test]
574 fn test_parse_circle_farthest_side() {
575 assert_eq!(
576 BasicShape::from_str("circle(farthest-side)"),
577 Ok(BasicShape::Ellipse(Box::new(EllipseShape {
578 radius_x: ShapeRadius::FarthestSide,
579 radius_y: ShapeRadius::FarthestSide,
580 position: ShapePosition::default(),
581 })))
582 );
583 }
584}