float_pigment_css/sheet/
media.rs

1use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
2
3use crate::{length_num::LengthNum, query::MediaQueryStatus};
4
5#[cfg(debug_assertions)]
6use float_pigment_css_macro::CompatibilityEnumCheck;
7
8#[derive(Debug, Clone)]
9pub(crate) struct Media {
10    pub(crate) parent: Option<Rc<Media>>,
11    pub(crate) media_queries: Vec<MediaQuery>,
12}
13
14#[derive(Debug, Clone)]
15pub(crate) struct MediaQuery {
16    pub(crate) decorator: MediaTypeDecorator,
17    pub(crate) cond: Vec<MediaExpression>,
18}
19
20#[repr(C)]
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22#[cfg_attr(debug_assertions, derive(CompatibilityEnumCheck))]
23pub(crate) enum MediaExpression {
24    Unknown,
25    MediaType(MediaType),
26    Orientation(Orientation),
27    Width(f32),
28    MinWidth(f32),
29    MaxWidth(f32),
30    Height(f32),
31    MinHeight(f32),
32    MaxHeight(f32),
33    Theme(Theme),
34}
35
36#[repr(C)]
37#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
38#[cfg_attr(debug_assertions, derive(CompatibilityEnumCheck))]
39pub(crate) enum MediaTypeDecorator {
40    None,
41    Not,
42    Only,
43}
44
45#[repr(C)]
46#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
47#[cfg_attr(debug_assertions, derive(CompatibilityEnumCheck))]
48pub(crate) enum MediaType {
49    None,
50    All,
51    Screen,
52}
53
54#[repr(C)]
55#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
56#[cfg_attr(debug_assertions, derive(CompatibilityEnumCheck))]
57pub(crate) enum Orientation {
58    None,
59    Portrait,
60    Landscape,
61}
62
63/// The current theme of the system environment, e.g. dark mode.
64#[repr(C)]
65#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
66#[cfg_attr(debug_assertions, derive(CompatibilityEnumCheck))]
67pub enum Theme {
68    /// Unspecified.
69    None,
70    /// Light mode.
71    Light,
72    /// Dark mode.
73    Dark,
74}
75
76impl Media {
77    pub(crate) fn new(parent: Option<Rc<Media>>) -> Self {
78        Self {
79            parent,
80            media_queries: vec![],
81        }
82    }
83
84    pub(crate) fn add_media_query(&mut self, mq: MediaQuery) {
85        self.media_queries.push(mq)
86    }
87
88    pub(crate) fn is_valid<L: LengthNum>(&self, mqs: &MediaQueryStatus<L>) -> bool {
89        if let Some(parent) = &self.parent {
90            if !parent.is_valid(mqs) {
91                return false;
92            }
93        }
94        for mq in self.media_queries.iter() {
95            if mq.is_valid(mqs) {
96                return true;
97            }
98        }
99        false
100    }
101
102    pub(crate) fn to_media_query_string_list(&self, list: &mut Vec<String>) {
103        if let Some(p) = &self.parent {
104            p.to_media_query_string_list(list);
105        }
106        list.push(
107            self.media_queries
108                .iter()
109                .map(|x| x.to_media_query_string())
110                .collect::<Box<[String]>>()
111                .join(", "),
112        )
113    }
114}
115
116impl MediaQuery {
117    pub(crate) fn new() -> Self {
118        Self {
119            decorator: MediaTypeDecorator::None,
120            cond: vec![],
121        }
122    }
123
124    pub(crate) fn set_decorator(&mut self, d: MediaTypeDecorator) {
125        self.decorator = d;
126    }
127
128    pub(crate) fn add_media_expression(&mut self, mq: MediaExpression) {
129        self.cond.push(mq)
130    }
131
132    fn is_valid<L: LengthNum>(&self, mqs: &MediaQueryStatus<L>) -> bool {
133        let allow_unknown = self.decorator != MediaTypeDecorator::Only;
134        for cond in self.cond.iter() {
135            let mut matched = match cond {
136                MediaExpression::Unknown => allow_unknown,
137                MediaExpression::MediaType(mt) => match mt {
138                    MediaType::None => false,
139                    MediaType::All => true,
140                    MediaType::Screen => mqs.is_screen,
141                },
142                MediaExpression::Orientation(o) => match o {
143                    Orientation::None => false,
144                    Orientation::Portrait => mqs.width <= mqs.height,
145                    Orientation::Landscape => mqs.width > mqs.height,
146                },
147                MediaExpression::Width(x) => mqs.width.to_f32() == *x,
148                MediaExpression::MinWidth(x) => mqs.width.to_f32() >= *x,
149                MediaExpression::MaxWidth(x) => mqs.width.to_f32() <= *x,
150                MediaExpression::Height(x) => mqs.height.to_f32() == *x,
151                MediaExpression::MinHeight(x) => mqs.height.to_f32() >= *x,
152                MediaExpression::MaxHeight(x) => mqs.height.to_f32() <= *x,
153                MediaExpression::Theme(t) => match t {
154                    Theme::None => false,
155                    Theme::Light => mqs.theme == Theme::Light,
156                    Theme::Dark => mqs.theme == Theme::Dark,
157                },
158            };
159            if self.decorator == MediaTypeDecorator::Not {
160                matched = !matched;
161            }
162            if !matched {
163                return false;
164            }
165        }
166        true
167    }
168
169    pub(crate) fn to_media_query_string(&self) -> String {
170        let decorator = match self.decorator {
171            MediaTypeDecorator::None => "",
172            MediaTypeDecorator::Not => "not ",
173            MediaTypeDecorator::Only => "only ",
174        };
175        let cond = self
176            .cond
177            .iter()
178            .map(|cond| match cond {
179                MediaExpression::Unknown => "unknown".into(),
180                MediaExpression::MediaType(mt) => match mt {
181                    MediaType::None => "none".into(),
182                    MediaType::All => "all".into(),
183                    MediaType::Screen => "screen".into(),
184                },
185                MediaExpression::Orientation(o) => match o {
186                    Orientation::None => "(orientation: none)",
187                    Orientation::Portrait => "(orientation: portrait)",
188                    Orientation::Landscape => "(orientation: landscape)",
189                }
190                .into(),
191                MediaExpression::Width(x) => format!("(width: {}px)", x),
192                MediaExpression::MinWidth(x) => format!("(min-width: {}px)", x),
193                MediaExpression::MaxWidth(x) => format!("(max-width: {}px)", x),
194                MediaExpression::Height(x) => format!("(height: {}px)", x),
195                MediaExpression::MinHeight(x) => format!("(min-height: {}px)", x),
196                MediaExpression::MaxHeight(x) => format!("(max-height: {}px)", x),
197                MediaExpression::Theme(t) => match t {
198                    Theme::None => "(prefers-color-scheme: none)",
199                    Theme::Light => "(prefers-color-scheme: light)",
200                    Theme::Dark => "(prefers-color-scheme: dark)",
201                }
202                .into(),
203            })
204            .collect::<Box<[String]>>()
205            .join(" and ");
206        format!("{}{}", decorator, cond)
207    }
208}