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 ident = input.expect_ident()?;
437    match_ignore_ascii_case! { &ident,
438      "normal" => return Ok(res),
439      "only" => res |= ColorScheme::Only,
440      "light" => res |= ColorScheme::Light,
441      "dark" => res |= ColorScheme::Dark,
442      _ => {}
443    };
444
445    while let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) {
446      match_ignore_ascii_case! { &ident,
447        "normal" => return Err(input.new_custom_error(ParserError::InvalidValue)),
448        "only" => {
449          // Only must be at the start or the end, not in the middle.
450          if res.contains(ColorScheme::Only) {
451            return Err(input.new_custom_error(ParserError::InvalidValue));
452          }
453          res |= ColorScheme::Only;
454          return Ok(res);
455        },
456        "light" => res |= ColorScheme::Light,
457        "dark" => res |= ColorScheme::Dark,
458        _ => {}
459      };
460    }
461
462    Ok(res)
463  }
464}
465
466impl ToCss for ColorScheme {
467  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
468  where
469    W: std::fmt::Write,
470  {
471    if self.is_empty() {
472      return dest.write_str("normal");
473    }
474
475    if self.contains(ColorScheme::Light) {
476      dest.write_str("light")?;
477      if self.contains(ColorScheme::Dark) {
478        dest.write_char(' ')?;
479      }
480    }
481
482    if self.contains(ColorScheme::Dark) {
483      dest.write_str("dark")?;
484    }
485
486    if self.contains(ColorScheme::Only) {
487      dest.write_str(" only")?;
488    }
489
490    Ok(())
491  }
492}
493
494#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
495#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
496struct SerializedColorScheme {
497  light: bool,
498  dark: bool,
499  only: bool,
500}
501
502impl From<ColorScheme> for SerializedColorScheme {
503  fn from(color_scheme: ColorScheme) -> Self {
504    Self {
505      light: color_scheme.contains(ColorScheme::Light),
506      dark: color_scheme.contains(ColorScheme::Dark),
507      only: color_scheme.contains(ColorScheme::Only),
508    }
509  }
510}
511
512impl From<SerializedColorScheme> for ColorScheme {
513  fn from(s: SerializedColorScheme) -> ColorScheme {
514    let mut color_scheme = ColorScheme::empty();
515    color_scheme.set(ColorScheme::Light, s.light);
516    color_scheme.set(ColorScheme::Dark, s.dark);
517    color_scheme.set(ColorScheme::Only, s.only);
518    color_scheme
519  }
520}
521
522#[cfg(feature = "jsonschema")]
523#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
524impl<'a> schemars::JsonSchema for ColorScheme {
525  fn is_referenceable() -> bool {
526    true
527  }
528
529  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
530    SerializedColorScheme::json_schema(gen)
531  }
532
533  fn schema_name() -> String {
534    "ColorScheme".into()
535  }
536}
537
538#[derive(Default)]
539pub(crate) struct ColorSchemeHandler;
540
541impl<'i> PropertyHandler<'i> for ColorSchemeHandler {
542  fn handle_property(
543    &mut self,
544    property: &Property<'i>,
545    dest: &mut DeclarationList<'i>,
546    context: &mut PropertyHandlerContext<'i, '_>,
547  ) -> bool {
548    match property {
549      Property::ColorScheme(color_scheme) => {
550        if should_compile!(context.targets, LightDark) {
551          if color_scheme.contains(ColorScheme::Light) {
552            dest.push(define_var("--lightningcss-light", Token::Ident("initial".into())));
553            dest.push(define_var("--lightningcss-dark", Token::WhiteSpace(" ".into())));
554
555            if color_scheme.contains(ColorScheme::Dark) {
556              context.add_dark_rule(define_var("--lightningcss-light", Token::WhiteSpace(" ".into())));
557              context.add_dark_rule(define_var("--lightningcss-dark", Token::Ident("initial".into())));
558            }
559          } else if color_scheme.contains(ColorScheme::Dark) {
560            dest.push(define_var("--lightningcss-light", Token::WhiteSpace(" ".into())));
561            dest.push(define_var("--lightningcss-dark", Token::Ident("initial".into())));
562          }
563        }
564        dest.push(property.clone());
565        true
566      }
567      _ => false,
568    }
569  }
570
571  fn finalize(&mut self, _: &mut DeclarationList<'i>, _: &mut PropertyHandlerContext<'i, '_>) {}
572}
573
574#[inline]
575fn define_var<'i>(name: &'static str, value: Token<'static>) -> Property<'i> {
576  Property::Custom(CustomProperty {
577    name: CustomPropertyName::Custom(name.into()),
578    value: TokenList(vec![TokenOrValue::Token(value)]),
579  })
580}