Skip to main content

lightningcss/properties/
ui.rs

1//! CSS properties related to user interface.
2
3use crate::context::PropertyHandlerContext;
4use crate::declaration::{DeclarationBlock, DeclarationList};
5use crate::error::{ParserError, PrinterError};
6use crate::macros::{define_shorthand, enum_property, shorthand_property};
7use crate::printer::Printer;
8use crate::properties::{Property, PropertyId};
9use crate::targets::{should_compile, Browsers, Targets};
10use crate::traits::{FallbackValues, IsCompatible, Parse, PropertyHandler, Shorthand, ToCss};
11use crate::values::color::CssColor;
12use crate::values::number::CSSNumber;
13use crate::values::string::CowArcStr;
14use crate::values::url::Url;
15#[cfg(feature = "visitor")]
16use crate::visitor::Visit;
17use bitflags::bitflags;
18use cssparser::*;
19use smallvec::SmallVec;
20
21use super::custom::Token;
22use super::{CustomProperty, CustomPropertyName, TokenList, TokenOrValue};
23
24enum_property! {
25  /// A value for the [resize](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#resize) property.
26  pub enum Resize {
27    /// The element does not allow resizing.
28    None,
29    /// The element is resizable in both the x and y directions.
30    Both,
31    /// The element is resizable in the x direction.
32    Horizontal,
33    /// The element is resizable in the y direction.
34    Vertical,
35    /// The element is resizable in the block direction, according to the writing mode.
36    Block,
37    /// The element is resizable in the inline direction, according to the writing mode.
38    Inline,
39  }
40}
41
42/// A [cursor image](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value, used in the `cursor` property.
43///
44/// See [Cursor](Cursor).
45#[derive(Debug, Clone, PartialEq)]
46#[cfg_attr(feature = "visitor", derive(Visit))]
47#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
50pub struct CursorImage<'i> {
51  /// A url to the cursor image.
52  #[cfg_attr(feature = "serde", serde(borrow))]
53  pub url: Url<'i>,
54  /// The location in the image where the mouse pointer appears.
55  pub hotspot: Option<(CSSNumber, CSSNumber)>,
56}
57
58impl<'i> Parse<'i> for CursorImage<'i> {
59  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
60    let url = Url::parse(input)?;
61    let hotspot = if let Ok(x) = input.try_parse(CSSNumber::parse) {
62      let y = CSSNumber::parse(input)?;
63      Some((x, y))
64    } else {
65      None
66    };
67
68    Ok(CursorImage { url, hotspot })
69  }
70}
71
72impl<'i> ToCss for CursorImage<'i> {
73  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
74  where
75    W: std::fmt::Write,
76  {
77    self.url.to_css(dest)?;
78
79    if let Some((x, y)) = self.hotspot {
80      dest.write_char(' ')?;
81      x.to_css(dest)?;
82      dest.write_char(' ')?;
83      y.to_css(dest)?;
84    }
85    Ok(())
86  }
87}
88
89enum_property! {
90  /// A pre-defined [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) value,
91  /// used in the `cursor` property.
92  ///
93  /// See [Cursor](Cursor).
94  #[allow(missing_docs)]
95  pub enum CursorKeyword {
96    Auto,
97    Default,
98    None,
99    ContextMenu,
100    Help,
101    Pointer,
102    Progress,
103    Wait,
104    Cell,
105    Crosshair,
106    Text,
107    VerticalText,
108    Alias,
109    Copy,
110    Move,
111    NoDrop,
112    NotAllowed,
113    Grab,
114    Grabbing,
115    EResize,
116    NResize,
117    NeResize,
118    NwResize,
119    SResize,
120    SeResize,
121    SwResize,
122    WResize,
123    EwResize,
124    NsResize,
125    NeswResize,
126    NwseResize,
127    ColResize,
128    RowResize,
129    AllScroll,
130    ZoomIn,
131    ZoomOut,
132  }
133}
134
135/// A value for the [cursor](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#cursor) property.
136#[derive(Debug, Clone, PartialEq)]
137#[cfg_attr(feature = "visitor", derive(Visit))]
138#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
139#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
140#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
141pub struct Cursor<'i> {
142  /// A list of cursor images.
143  #[cfg_attr(feature = "serde", serde(borrow))]
144  pub images: SmallVec<[CursorImage<'i>; 1]>,
145  /// A pre-defined cursor.
146  pub keyword: CursorKeyword,
147}
148
149impl<'i> Parse<'i> for Cursor<'i> {
150  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
151    let mut images = SmallVec::new();
152    loop {
153      match input.try_parse(CursorImage::parse) {
154        Ok(image) => images.push(image),
155        Err(_) => break,
156      }
157      input.expect_comma()?;
158    }
159
160    Ok(Cursor {
161      images,
162      keyword: CursorKeyword::parse(input)?,
163    })
164  }
165}
166
167impl<'i> ToCss for Cursor<'i> {
168  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
169  where
170    W: std::fmt::Write,
171  {
172    for image in &self.images {
173      image.to_css(dest)?;
174      dest.delim(',', false)?;
175    }
176    self.keyword.to_css(dest)
177  }
178}
179
180/// A value for the [caret-color](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-color) property.
181#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
182#[cfg_attr(feature = "visitor", derive(Visit))]
183#[cfg_attr(
184  feature = "serde",
185  derive(serde::Serialize, serde::Deserialize),
186  serde(tag = "type", content = "value", rename_all = "kebab-case")
187)]
188#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
189#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
190pub enum ColorOrAuto {
191  /// The `currentColor`, adjusted by the UA to ensure contrast against the background.
192  Auto,
193  /// A color.
194  Color(CssColor),
195}
196
197impl Default for ColorOrAuto {
198  fn default() -> ColorOrAuto {
199    ColorOrAuto::Auto
200  }
201}
202
203impl FallbackValues for ColorOrAuto {
204  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
205    match self {
206      ColorOrAuto::Color(color) => color
207        .get_fallbacks(targets)
208        .into_iter()
209        .map(|color| ColorOrAuto::Color(color))
210        .collect(),
211      ColorOrAuto::Auto => Vec::new(),
212    }
213  }
214}
215
216impl IsCompatible for ColorOrAuto {
217  fn is_compatible(&self, browsers: Browsers) -> bool {
218    match self {
219      ColorOrAuto::Color(color) => color.is_compatible(browsers),
220      ColorOrAuto::Auto => true,
221    }
222  }
223}
224
225enum_property! {
226  /// A value for the [caret-shape](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret-shape) property.
227  pub enum CaretShape {
228    /// The UA determines the caret shape.
229    Auto,
230    /// A thin bar caret.
231    Bar,
232    /// A rectangle caret.
233    Block,
234    /// An underscore caret.
235    Underscore,
236  }
237}
238
239impl Default for CaretShape {
240  fn default() -> CaretShape {
241    CaretShape::Auto
242  }
243}
244
245shorthand_property! {
246  /// A value for the [caret](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#caret) shorthand property.
247  pub struct Caret {
248    /// The caret color.
249    color: CaretColor(ColorOrAuto),
250    /// The caret shape.
251    shape: CaretShape(CaretShape),
252  }
253}
254
255impl FallbackValues for Caret {
256  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
257    self
258      .color
259      .get_fallbacks(targets)
260      .into_iter()
261      .map(|color| Caret {
262        color,
263        shape: self.shape.clone(),
264      })
265      .collect()
266  }
267}
268
269impl IsCompatible for Caret {
270  fn is_compatible(&self, browsers: Browsers) -> bool {
271    self.color.is_compatible(browsers)
272  }
273}
274
275enum_property! {
276  /// A value for the [user-select](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#content-selection) property.
277  pub enum UserSelect {
278    /// The UA determines whether text is selectable.
279    Auto,
280    /// Text is selectable.
281    Text,
282    /// Text is not selectable.
283    None,
284    /// Text selection is contained to the element.
285    Contain,
286    /// Only the entire element is selectable.
287    All,
288  }
289}
290
291/// A value for the [appearance](https://www.w3.org/TR/2021/WD-css-ui-4-20210316/#appearance-switching) property.
292#[derive(Debug, Clone, PartialEq)]
293#[cfg_attr(feature = "visitor", derive(Visit))]
294#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
295#[allow(missing_docs)]
296pub enum Appearance<'i> {
297  None,
298  Auto,
299  Textfield,
300  MenulistButton,
301  Button,
302  Checkbox,
303  Listbox,
304  Menulist,
305  Meter,
306  ProgressBar,
307  PushButton,
308  Radio,
309  Searchfield,
310  SliderHorizontal,
311  SquareButton,
312  Textarea,
313  NonStandard(CowArcStr<'i>),
314}
315
316impl<'i> Appearance<'i> {
317  fn from_str(name: &str) -> Option<Self> {
318    Some(match_ignore_ascii_case! { &name,
319      "none" => Appearance::None,
320      "auto" => Appearance::Auto,
321      "textfield" => Appearance::Textfield,
322      "menulist-button" => Appearance::MenulistButton,
323      "button" => Appearance::Button,
324      "checkbox" => Appearance::Checkbox,
325      "listbox" => Appearance::Listbox,
326      "menulist" => Appearance::Menulist,
327      "meter" => Appearance::Meter,
328      "progress-bar" => Appearance::ProgressBar,
329      "push-button" => Appearance::PushButton,
330      "radio" => Appearance::Radio,
331      "searchfield" => Appearance::Searchfield,
332      "slider-horizontal" => Appearance::SliderHorizontal,
333      "square-button" => Appearance::SquareButton,
334      "textarea" => Appearance::Textarea,
335      _ => return None
336    })
337  }
338
339  fn to_str(&self) -> &str {
340    match self {
341      Appearance::None => "none",
342      Appearance::Auto => "auto",
343      Appearance::Textfield => "textfield",
344      Appearance::MenulistButton => "menulist-button",
345      Appearance::Button => "button",
346      Appearance::Checkbox => "checkbox",
347      Appearance::Listbox => "listbox",
348      Appearance::Menulist => "menulist",
349      Appearance::Meter => "meter",
350      Appearance::ProgressBar => "progress-bar",
351      Appearance::PushButton => "push-button",
352      Appearance::Radio => "radio",
353      Appearance::Searchfield => "searchfield",
354      Appearance::SliderHorizontal => "slider-horizontal",
355      Appearance::SquareButton => "square-button",
356      Appearance::Textarea => "textarea",
357      Appearance::NonStandard(s) => s.as_ref(),
358    }
359  }
360}
361
362impl<'i> Parse<'i> for Appearance<'i> {
363  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
364    let ident = input.expect_ident()?;
365    Ok(Self::from_str(ident.as_ref()).unwrap_or_else(|| Appearance::NonStandard(ident.into())))
366  }
367}
368
369impl<'i> ToCss for Appearance<'i> {
370  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
371  where
372    W: std::fmt::Write,
373  {
374    dest.write_str(self.to_str())
375  }
376}
377
378#[cfg(feature = "serde")]
379#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
380impl<'i> serde::Serialize for Appearance<'i> {
381  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
382  where
383    S: serde::Serializer,
384  {
385    serializer.serialize_str(self.to_str())
386  }
387}
388
389#[cfg(feature = "serde")]
390#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
391impl<'i, 'de: 'i> serde::Deserialize<'de> for Appearance<'i> {
392  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
393  where
394    D: serde::Deserializer<'de>,
395  {
396    let s = CowArcStr::deserialize(deserializer)?;
397    Ok(Self::from_str(s.as_ref()).unwrap_or_else(|| Appearance::NonStandard(s)))
398  }
399}
400
401#[cfg(feature = "jsonschema")]
402#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
403impl<'a> schemars::JsonSchema for Appearance<'a> {
404  fn is_referenceable() -> bool {
405    true
406  }
407
408  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
409    str::json_schema(gen)
410  }
411
412  fn schema_name() -> String {
413    "Appearance".into()
414  }
415}
416
417bitflags! {
418  /// A value for the [color-scheme](https://drafts.csswg.org/css-color-adjust/#color-scheme-prop) property.
419  #[cfg_attr(feature = "visitor", derive(Visit))]
420  #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(from = "SerializedColorScheme", into = "SerializedColorScheme"))]
421  #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
422  #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
423  pub struct ColorScheme: u8 {
424    /// Indicates that the element supports a light color scheme.
425    const Light    = 0b01;
426    /// Indicates that the element supports a dark color scheme.
427    const Dark     = 0b10;
428    /// Forbids the user agent from overriding the color scheme for the element.
429    const Only     = 0b100;
430  }
431}
432
433impl<'i> Parse<'i> for ColorScheme {
434  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
435    let mut res = ColorScheme::empty();
436    let mut has_any = false;
437
438    if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
439      return Ok(res);
440    }
441
442    if input.try_parse(|input| input.expect_ident_matching("only")).is_ok() {
443      res |= ColorScheme::Only;
444      has_any = true;
445    }
446
447    loop {
448      if input.try_parse(|input| input.expect_ident_matching("light")).is_ok() {
449        res |= ColorScheme::Light;
450        has_any = true;
451        continue;
452      }
453
454      if input.try_parse(|input| input.expect_ident_matching("dark")).is_ok() {
455        res |= ColorScheme::Dark;
456        has_any = true;
457        continue;
458      }
459
460      break;
461    }
462
463    // Only is allowed at the start or the end.
464    if !res.contains(ColorScheme::Only) && input.try_parse(|input| input.expect_ident_matching("only")).is_ok() {
465      res |= ColorScheme::Only;
466      has_any = true;
467    }
468
469    if has_any {
470      return Ok(res);
471    }
472
473    Err(input.new_custom_error(ParserError::InvalidValue))
474  }
475}
476
477impl ToCss for ColorScheme {
478  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
479  where
480    W: std::fmt::Write,
481  {
482    if self.is_empty() {
483      return dest.write_str("normal");
484    }
485
486    if self.contains(ColorScheme::Light) {
487      dest.write_str("light")?;
488      if self.contains(ColorScheme::Dark) {
489        dest.write_char(' ')?;
490      }
491    }
492
493    if self.contains(ColorScheme::Dark) {
494      dest.write_str("dark")?;
495    }
496
497    if self.contains(ColorScheme::Only) {
498      // Avoid parsing `color-scheme: only` as `color-scheme:  only`
499      if !self.intersects(ColorScheme::Light | ColorScheme::Dark) {
500        return dest.write_str("only");
501      }
502      dest.write_str(" only")?;
503    }
504
505    Ok(())
506  }
507}
508
509#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
510#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
511struct SerializedColorScheme {
512  light: bool,
513  dark: bool,
514  only: bool,
515}
516
517impl From<ColorScheme> for SerializedColorScheme {
518  fn from(color_scheme: ColorScheme) -> Self {
519    Self {
520      light: color_scheme.contains(ColorScheme::Light),
521      dark: color_scheme.contains(ColorScheme::Dark),
522      only: color_scheme.contains(ColorScheme::Only),
523    }
524  }
525}
526
527impl From<SerializedColorScheme> for ColorScheme {
528  fn from(s: SerializedColorScheme) -> ColorScheme {
529    let mut color_scheme = ColorScheme::empty();
530    color_scheme.set(ColorScheme::Light, s.light);
531    color_scheme.set(ColorScheme::Dark, s.dark);
532    color_scheme.set(ColorScheme::Only, s.only);
533    color_scheme
534  }
535}
536
537#[cfg(feature = "jsonschema")]
538#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
539impl<'a> schemars::JsonSchema for ColorScheme {
540  fn is_referenceable() -> bool {
541    true
542  }
543
544  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
545    SerializedColorScheme::json_schema(gen)
546  }
547
548  fn schema_name() -> String {
549    "ColorScheme".into()
550  }
551}
552
553#[derive(Default)]
554pub(crate) struct ColorSchemeHandler;
555
556impl<'i> PropertyHandler<'i> for ColorSchemeHandler {
557  fn handle_property(
558    &mut self,
559    property: &Property<'i>,
560    dest: &mut DeclarationList<'i>,
561    context: &mut PropertyHandlerContext<'i, '_>,
562  ) -> bool {
563    match property {
564      Property::ColorScheme(color_scheme) => {
565        if should_compile!(context.targets, LightDark) {
566          if color_scheme.contains(ColorScheme::Light) {
567            dest.push(define_var("--lightningcss-light", Token::Ident("initial".into())));
568            dest.push(define_var("--lightningcss-dark", Token::WhiteSpace(" ".into())));
569
570            if color_scheme.contains(ColorScheme::Dark) {
571              context.add_dark_rule(define_var("--lightningcss-light", Token::WhiteSpace(" ".into())));
572              context.add_dark_rule(define_var("--lightningcss-dark", Token::Ident("initial".into())));
573            }
574          } else if color_scheme.contains(ColorScheme::Dark) {
575            dest.push(define_var("--lightningcss-light", Token::WhiteSpace(" ".into())));
576            dest.push(define_var("--lightningcss-dark", Token::Ident("initial".into())));
577          }
578        }
579        dest.push(property.clone());
580        true
581      }
582      _ => false,
583    }
584  }
585
586  fn finalize(&mut self, _: &mut DeclarationList<'i>, _: &mut PropertyHandlerContext<'i, '_>) {}
587}
588
589enum_property! {
590  /// A value for the [print-color-adjust](https://drafts.csswg.org/css-color-adjust/#propdef-print-color-adjust) property.
591  pub enum PrintColorAdjust {
592    /// The user agent is allowed to make adjustments to the element as it deems appropriate.
593    Economy,
594    /// The user agent is not allowed to make adjustments to the element.
595    Exact,
596  }
597}
598
599#[inline]
600fn define_var<'i>(name: &'static str, value: Token<'static>) -> Property<'i> {
601  Property::Custom(CustomProperty {
602    name: CustomPropertyName::Custom(name.into()),
603    value: TokenList(vec![TokenOrValue::Token(value)]),
604  })
605}