blob: 61373cb87d50802e6b526a0e5a400a686539e937 [file] [log] [blame]
//! CSS position values.
use super::length::LengthPercentage;
use super::percentage::Percentage;
use crate::error::{ParserError, PrinterError};
use crate::macros::enum_property;
use crate::printer::Printer;
use crate::targets::Browsers;
use crate::traits::{IsCompatible, Parse, ToCss, Zero};
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;
#[cfg(feature = "serde")]
use crate::serialization::ValueWrapper;
/// A CSS [`<position>`](https://www.w3.org/TR/css3-values/#position) value,
/// as used in the `background-position` property, gradients, masks, etc.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct Position {
/// The x-position.
pub x: HorizontalPosition,
/// The y-position.
pub y: VerticalPosition,
}
impl Position {
/// Returns a `Position` with both the x and y set to `center`.
pub fn center() -> Position {
Position {
x: HorizontalPosition::Center,
y: VerticalPosition::Center,
}
}
/// Returns whether both the x and y positions are centered.
pub fn is_center(&self) -> bool {
self.x.is_center() && self.y.is_center()
}
/// Returns whether both the x and y positions are zero.
pub fn is_zero(&self) -> bool {
self.x.is_zero() && self.y.is_zero()
}
}
impl Default for Position {
fn default() -> Position {
Position {
x: HorizontalPosition::Length(LengthPercentage::Percentage(Percentage(0.0))),
y: VerticalPosition::Length(LengthPercentage::Percentage(Percentage(0.0))),
}
}
}
impl<'i> Parse<'i> for Position {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
match input.try_parse(HorizontalPosition::parse) {
Ok(HorizontalPosition::Center) => {
// Try parsing a vertical position next.
if let Ok(y) = input.try_parse(VerticalPosition::parse) {
return Ok(Position {
x: HorizontalPosition::Center,
y,
});
}
// If it didn't work, assume the first actually represents a y position,
// and the next is an x position. e.g. `center left` rather than `left center`.
let x = input.try_parse(HorizontalPosition::parse).unwrap_or(HorizontalPosition::Center);
let y = VerticalPosition::Center;
return Ok(Position { x, y });
}
Ok(x @ HorizontalPosition::Length(_)) => {
// If we got a length as the first component, then the second must
// be a keyword or length (not a side offset).
if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
let y = VerticalPosition::Side {
side: y_keyword,
offset: None,
};
return Ok(Position { x, y });
}
if let Ok(y_lp) = input.try_parse(LengthPercentage::parse) {
let y = VerticalPosition::Length(y_lp);
return Ok(Position { x, y });
}
let y = VerticalPosition::Center;
let _ = input.try_parse(|i| i.expect_ident_matching("center"));
return Ok(Position { x, y });
}
Ok(HorizontalPosition::Side {
side: x_keyword,
offset: lp,
}) => {
// If we got a horizontal side keyword (and optional offset), expect another for the vertical side.
// e.g. `left center` or `left 20px center`
if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
let x = HorizontalPosition::Side {
side: x_keyword,
offset: lp,
};
let y = VerticalPosition::Center;
return Ok(Position { x, y });
}
// e.g. `left top`, `left top 20px`, `left 20px top`, or `left 20px top 20px`
if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
let y_lp = input.try_parse(LengthPercentage::parse).ok();
let x = HorizontalPosition::Side {
side: x_keyword,
offset: lp,
};
let y = VerticalPosition::Side {
side: y_keyword,
offset: y_lp,
};
return Ok(Position { x, y });
}
// If we didn't get a vertical side keyword (e.g. `left 20px`), then apply the offset to the vertical side.
let x = HorizontalPosition::Side {
side: x_keyword,
offset: None,
};
let y = lp.map_or(VerticalPosition::Center, VerticalPosition::Length);
return Ok(Position { x, y });
}
_ => {}
}
// If the horizontal position didn't parse, then it must be out of order. Try vertical position keyword.
let y_keyword = VerticalPositionKeyword::parse(input)?;
let lp_and_x_pos: Result<_, ParseError<()>> = input.try_parse(|i| {
let y_lp = i.try_parse(LengthPercentage::parse).ok();
if let Ok(x_keyword) = i.try_parse(HorizontalPositionKeyword::parse) {
let x_lp = i.try_parse(LengthPercentage::parse).ok();
let x_pos = HorizontalPosition::Side {
side: x_keyword,
offset: x_lp,
};
return Ok((y_lp, x_pos));
}
i.expect_ident_matching("center")?;
let x_pos = HorizontalPosition::Center;
Ok((y_lp, x_pos))
});
if let Ok((y_lp, x)) = lp_and_x_pos {
let y = VerticalPosition::Side {
side: y_keyword,
offset: y_lp,
};
return Ok(Position { x, y });
}
let x = HorizontalPosition::Center;
let y = VerticalPosition::Side {
side: y_keyword,
offset: None,
};
Ok(Position { x, y })
}
}
impl ToCss for Position {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match (&self.x, &self.y) {
(x_pos @ &HorizontalPosition::Side { side, offset: Some(_) }, &VerticalPosition::Length(ref y_lp))
if side != HorizontalPositionKeyword::Left =>
{
x_pos.to_css(dest)?;
dest.write_str(" top ")?;
y_lp.to_css(dest)
}
(x_pos @ &HorizontalPosition::Side { side, offset: Some(_) }, y)
if side != HorizontalPositionKeyword::Left && y.is_center() =>
{
// If there is a side keyword with an offset, "center" must be a keyword not a percentage.
x_pos.to_css(dest)?;
dest.write_str(" center")
}
(&HorizontalPosition::Length(ref x_lp), y_pos @ &VerticalPosition::Side { side, offset: Some(_) })
if side != VerticalPositionKeyword::Top =>
{
dest.write_str("left ")?;
x_lp.to_css(dest)?;
dest.write_str(" ")?;
y_pos.to_css(dest)
}
(x, y) if x.is_center() && y.is_center() => {
// `center center` => 50%
x.to_css(dest)
}
(&HorizontalPosition::Length(ref x_lp), y) if y.is_center() => {
// `center` is assumed if omitted.
x_lp.to_css(dest)
}
(&HorizontalPosition::Side { side, offset: None }, y) if y.is_center() => {
let p: LengthPercentage = side.into();
p.to_css(dest)
}
(x, y_pos @ &VerticalPosition::Side { offset: None, .. }) if x.is_center() => y_pos.to_css(dest),
(&HorizontalPosition::Side { side: x, offset: None }, &VerticalPosition::Side { side: y, offset: None }) => {
let x: LengthPercentage = x.into();
let y: LengthPercentage = y.into();
x.to_css(dest)?;
dest.write_str(" ")?;
y.to_css(dest)
}
(x_pos, y_pos) => {
let zero = LengthPercentage::zero();
let fifty = LengthPercentage::Percentage(Percentage(0.5));
let x_len = match &x_pos {
HorizontalPosition::Side {
side: HorizontalPositionKeyword::Left,
offset,
} => {
if let Some(len) = offset {
if len.is_zero() {
Some(&zero)
} else {
Some(len)
}
} else {
Some(&zero)
}
}
HorizontalPosition::Length(len) if len.is_zero() => Some(&zero),
HorizontalPosition::Length(len) => Some(len),
HorizontalPosition::Center => Some(&fifty),
_ => None,
};
let y_len = match &y_pos {
VerticalPosition::Side {
side: VerticalPositionKeyword::Top,
offset,
} => {
if let Some(len) = offset {
if len.is_zero() {
Some(&zero)
} else {
Some(len)
}
} else {
Some(&zero)
}
}
VerticalPosition::Length(len) if len.is_zero() => Some(&zero),
VerticalPosition::Length(len) => Some(len),
VerticalPosition::Center => Some(&fifty),
_ => None,
};
if let (Some(x), Some(y)) = (x_len, y_len) {
x.to_css(dest)?;
dest.write_str(" ")?;
y.to_css(dest)
} else {
x_pos.to_css(dest)?;
dest.write_str(" ")?;
y_pos.to_css(dest)
}
}
}
}
}
impl IsCompatible for Position {
fn is_compatible(&self, _browsers: Browsers) -> bool {
true
}
}
/// A component within a [Position](Position) value, representing a position
/// along either the horizontal or vertical axis of a box.
///
/// This type is generic over side keywords.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum PositionComponent<S> {
/// The `center` keyword.
Center,
/// A length or percentage from the top-left corner of the box.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
Length(LengthPercentage),
/// A side keyword with an optional offset.
Side {
/// A side keyword.
side: S,
/// Offset from the side.
offset: Option<LengthPercentage>,
},
}
impl<S> PositionComponent<S> {
fn is_center(&self) -> bool {
match self {
PositionComponent::Center => true,
PositionComponent::Length(LengthPercentage::Percentage(Percentage(p))) => *p == 0.5,
_ => false,
}
}
fn is_zero(&self) -> bool {
matches!(self, PositionComponent::Length(len) if len.is_zero())
}
}
impl<'i, S: Parse<'i>> Parse<'i> for PositionComponent<S> {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
return Ok(PositionComponent::Center);
}
if let Ok(lp) = input.try_parse(|input| LengthPercentage::parse(input)) {
return Ok(PositionComponent::Length(lp));
}
let side = S::parse(input)?;
let offset = input.try_parse(|input| LengthPercentage::parse(input)).ok();
Ok(PositionComponent::Side { side, offset })
}
}
impl<S: ToCss> ToCss for PositionComponent<S> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
use PositionComponent::*;
match &self {
Center => {
if dest.minify {
dest.write_str("50%")
} else {
dest.write_str("center")
}
}
Length(lp) => lp.to_css(dest),
Side { side, offset } => {
side.to_css(dest)?;
if let Some(lp) = offset {
dest.write_str(" ")?;
lp.to_css(dest)?;
}
Ok(())
}
}
}
}
enum_property! {
/// A horizontal position keyword.
pub enum HorizontalPositionKeyword {
/// The `left` keyword.
Left,
/// The `right` keyword.
Right,
}
}
impl Into<LengthPercentage> for HorizontalPositionKeyword {
fn into(self) -> LengthPercentage {
match self {
HorizontalPositionKeyword::Left => LengthPercentage::zero(),
HorizontalPositionKeyword::Right => LengthPercentage::Percentage(Percentage(1.0)),
}
}
}
enum_property! {
/// A vertical position keyword.
pub enum VerticalPositionKeyword {
/// The `top` keyword.
Top,
/// The `bottom` keyword.
Bottom,
}
}
impl Into<LengthPercentage> for VerticalPositionKeyword {
fn into(self) -> LengthPercentage {
match self {
VerticalPositionKeyword::Top => LengthPercentage::zero(),
VerticalPositionKeyword::Bottom => LengthPercentage::Percentage(Percentage(1.0)),
}
}
}
/// A horizontal position component.
pub type HorizontalPosition = PositionComponent<HorizontalPositionKeyword>;
/// A vertical position component.
pub type VerticalPosition = PositionComponent<VerticalPositionKeyword>;