lightningcss/properties/
ui.rs

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