Skip to main content

hypersig_style/
condition.rs

1//! The conditions a part of a [`Style`](crate::Style) applies under.
2
3use crate::generated::{PseudoClass, PseudoElement};
4
5/// A media feature a part of a style applies under.
6///
7/// The breakpoints are min-width queries in `rem`, so a style for one applies
8/// from that width up, and a later breakpoint overrides an earlier one. Their
9/// widths are the default breakpoints of Tailwind CSS and Panda CSS (640, 768,
10/// 1024, 1280, and 1536 pixels at a 16px root font size), so a layout ported
11/// from either keeps its widths.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13#[non_exhaustive]
14pub enum Media {
15    /// From 40rem wide.
16    Sm,
17    /// From 48rem wide.
18    Md,
19    /// From 64rem wide.
20    Lg,
21    /// From 80rem wide.
22    Xl,
23    /// From 96rem wide.
24    Xxl,
25    /// The user prefers a dark color scheme.
26    Dark,
27    /// The user prefers a light color scheme.
28    Light,
29    /// The user asks for reduced motion.
30    ReducedMotion,
31    /// The page is printed.
32    Print,
33    /// The viewport is wider than it is tall.
34    Landscape,
35    /// The viewport is taller than it is wide.
36    Portrait,
37    /// The primary pointer can hover, as a mouse can and a finger cannot.
38    CanHover,
39    /// The primary pointer is coarse, such as a finger.
40    CoarsePointer,
41    /// The user agent forces a limited palette, as Windows high contrast does.
42    ForcedColors,
43    /// The user asks for more contrast.
44    MoreContrast,
45}
46
47impl Media {
48    /// The name this condition contributes to a class name.
49    pub(crate) fn name(self) -> &'static str {
50        match self {
51            Self::Sm => "sm",
52            Self::Md => "md",
53            Self::Lg => "lg",
54            Self::Xl => "xl",
55            Self::Xxl => "2xl",
56            Self::Dark => "dark",
57            Self::Light => "light",
58            Self::ReducedMotion => "motion-reduce",
59            Self::Print => "print",
60            Self::Landscape => "landscape",
61            Self::Portrait => "portrait",
62            Self::CanHover => "can-hover",
63            Self::CoarsePointer => "coarse-pointer",
64            Self::ForcedColors => "forced-colors",
65            Self::MoreContrast => "more-contrast",
66        }
67    }
68
69    /// The media query, without `@media`.
70    pub(crate) fn query(self) -> &'static str {
71        match self {
72            Self::Sm => "(min-width: 40rem)",
73            Self::Md => "(min-width: 48rem)",
74            Self::Lg => "(min-width: 64rem)",
75            Self::Xl => "(min-width: 80rem)",
76            Self::Xxl => "(min-width: 96rem)",
77            Self::Dark => "(prefers-color-scheme: dark)",
78            Self::Light => "(prefers-color-scheme: light)",
79            Self::ReducedMotion => "(prefers-reduced-motion: reduce)",
80            Self::Print => "print",
81            Self::Landscape => "(orientation: landscape)",
82            Self::Portrait => "(orientation: portrait)",
83            Self::CanHover => "(hover: hover)",
84            Self::CoarsePointer => "(pointer: coarse)",
85            Self::ForcedColors => "(forced-colors: active)",
86            Self::MoreContrast => "(prefers-contrast: more)",
87        }
88    }
89}
90
91/// A width of the nearest query container a part of a style applies from.
92///
93/// The widths are the default container sizes of Panda CSS, from 16rem to
94/// 90rem. An ancestor becomes a query container with `container-type`, as
95/// `Style::new().container_type(ContainerType::InlineSize)` sets it. Without
96/// one, the condition never matches.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
98#[non_exhaustive]
99pub enum Container {
100    /// From 16rem wide, Panda CSS's `@/3xs`.
101    Xs3,
102    /// From 18rem wide, Panda CSS's `@/2xs`.
103    Xs2,
104    /// From 20rem wide, Panda CSS's `@/xs`.
105    Xs,
106    /// From 24rem wide, Panda CSS's `@/sm`.
107    Sm,
108    /// From 28rem wide, Panda CSS's `@/md`.
109    Md,
110    /// From 32rem wide, Panda CSS's `@/lg`.
111    Lg,
112    /// From 36rem wide, Panda CSS's `@/xl`.
113    Xl,
114    /// From 42rem wide, Panda CSS's `@/2xl`.
115    Xl2,
116    /// From 48rem wide, Panda CSS's `@/3xl`.
117    Xl3,
118    /// From 56rem wide, Panda CSS's `@/4xl`.
119    Xl4,
120    /// From 64rem wide, Panda CSS's `@/5xl`.
121    Xl5,
122    /// From 72rem wide, Panda CSS's `@/6xl`.
123    Xl6,
124    /// From 80rem wide, Panda CSS's `@/7xl`.
125    Xl7,
126    /// From 90rem wide, Panda CSS's `@/8xl`.
127    Xl8,
128}
129
130impl Container {
131    /// The name this condition contributes to a class name.
132    pub(crate) fn name(self) -> &'static str {
133        match self {
134            Self::Xs3 => "@3xs",
135            Self::Xs2 => "@2xs",
136            Self::Xs => "@xs",
137            Self::Sm => "@sm",
138            Self::Md => "@md",
139            Self::Lg => "@lg",
140            Self::Xl => "@xl",
141            Self::Xl2 => "@2xl",
142            Self::Xl3 => "@3xl",
143            Self::Xl4 => "@4xl",
144            Self::Xl5 => "@5xl",
145            Self::Xl6 => "@6xl",
146            Self::Xl7 => "@7xl",
147            Self::Xl8 => "@8xl",
148        }
149    }
150
151    /// The container query, without `@container`.
152    pub(crate) fn query(self) -> &'static str {
153        match self {
154            Self::Xs3 => "(min-width: 16rem)",
155            Self::Xs2 => "(min-width: 18rem)",
156            Self::Xs => "(min-width: 20rem)",
157            Self::Sm => "(min-width: 24rem)",
158            Self::Md => "(min-width: 28rem)",
159            Self::Lg => "(min-width: 32rem)",
160            Self::Xl => "(min-width: 36rem)",
161            Self::Xl2 => "(min-width: 42rem)",
162            Self::Xl3 => "(min-width: 48rem)",
163            Self::Xl4 => "(min-width: 56rem)",
164            Self::Xl5 => "(min-width: 64rem)",
165            Self::Xl6 => "(min-width: 72rem)",
166            Self::Xl7 => "(min-width: 80rem)",
167            Self::Xl8 => "(min-width: 90rem)",
168        }
169    }
170}
171
172/// The user-action pseudo-classes in the order they are written, so that a
173/// later one wins while both match: `:active` over `:hover` while a button is
174/// pressed, and `:disabled` over all of them. The order follows the
175/// link-visited-hover-focus-active rule CSS authors use, extended with the
176/// focus variants and `:disabled`.
177const ACTION_ORDER: &[PseudoClass] = &[
178    PseudoClass::Link,
179    PseudoClass::Visited,
180    PseudoClass::FocusWithin,
181    PseudoClass::Hover,
182    PseudoClass::Focus,
183    PseudoClass::FocusVisible,
184    PseudoClass::Active,
185    PseudoClass::Disabled,
186];
187
188impl PseudoClass {
189    /// Where a rule for this pseudo-class is written relative to others.
190    pub(crate) fn rank(self) -> usize {
191        ACTION_ORDER
192            .iter()
193            .position(|&p| p == self)
194            .map_or(0, |i| i + 1)
195    }
196}
197
198/// A condition that [`Style::on`](crate::Style::on) applies a part of a style under.
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
200pub enum Condition {
201    /// The element matches a pseudo-class.
202    Pseudo(PseudoClass),
203    /// The document matches a media feature.
204    Media(Media),
205    /// The nearest query container is at least this wide.
206    Container(Container),
207    /// An ancestor marked with [`Style::group`](crate::Style::group) matches
208    /// a pseudo-class, as in `.group:hover .x`.
209    Group(PseudoClass),
210    /// An earlier sibling marked with [`Style::peer`](crate::Style::peer)
211    /// matches a pseudo-class, as in `.peer:checked ~ .x`.
212    Peer(PseudoClass),
213}
214
215impl From<Container> for Condition {
216    fn from(c: Container) -> Self {
217        Self::Container(c)
218    }
219}
220
221impl From<PseudoClass> for Condition {
222    fn from(p: PseudoClass) -> Self {
223        Self::Pseudo(p)
224    }
225}
226
227impl From<Media> for Condition {
228    fn from(m: Media) -> Self {
229        Self::Media(m)
230    }
231}
232
233/// The conditions of one declaration, in the order the stylesheet writes them.
234#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
235pub(crate) struct Conditions {
236    /// Sorted by enum order, so breakpoints come out ascending.
237    pub(crate) media: Vec<Media>,
238    /// Sorted by enum order, so widths come out ascending.
239    pub(crate) container: Vec<Container>,
240    /// Sorted by [`PseudoClass::rank`], then enum order, as are `peer` and `pseudo`.
241    pub(crate) group: Vec<(usize, PseudoClass)>,
242    pub(crate) peer: Vec<(usize, PseudoClass)>,
243    pub(crate) pseudo: Vec<(usize, PseudoClass)>,
244    pub(crate) element: Option<PseudoElement>,
245}
246
247/// The class [`Style::group`](crate::Style::group) marks an ancestor with.
248pub(crate) const GROUP: &str = "group";
249
250/// The class [`Style::peer`](crate::Style::peer) marks a sibling with.
251pub(crate) const PEER: &str = "peer";
252
253fn insert_ranked(list: &mut Vec<(usize, PseudoClass)>, p: PseudoClass) {
254    let key = (p.rank(), p);
255    if let Err(at) = list.binary_search(&key) {
256        list.insert(at, key);
257    }
258}
259
260impl Conditions {
261    pub(crate) fn add(&mut self, condition: Condition) {
262        match condition {
263            Condition::Media(m) => {
264                if let Err(at) = self.media.binary_search(&m) {
265                    self.media.insert(at, m);
266                }
267            }
268            Condition::Container(c) => {
269                if let Err(at) = self.container.binary_search(&c) {
270                    self.container.insert(at, c);
271                }
272            }
273            Condition::Pseudo(p) => insert_ranked(&mut self.pseudo, p),
274            Condition::Group(p) => insert_ranked(&mut self.group, p),
275            Condition::Peer(p) => insert_ranked(&mut self.peer, p),
276        }
277    }
278
279    /// Whether a group or peer condition is present, which a selector
280    /// without a class of its own cannot carry.
281    pub(crate) fn has_relation(&self) -> bool {
282        !self.group.is_empty() || !self.peer.is_empty()
283    }
284
285    /// Writes the selector for the element `target` writes: the group and
286    /// peer prefixes, the target, then its pseudo-classes and pseudo-element.
287    pub(crate) fn write_selector(&self, out: &mut String, target: impl FnOnce(&mut String)) {
288        if !self.group.is_empty() {
289            out.push('.');
290            out.push_str(GROUP);
291            for (_, p) in &self.group {
292                out.push(':');
293                out.push_str(p.name());
294            }
295            out.push(' ');
296        }
297        if !self.peer.is_empty() {
298            out.push('.');
299            out.push_str(PEER);
300            for (_, p) in &self.peer {
301                out.push(':');
302                out.push_str(p.name());
303            }
304            out.push_str(" ~ ");
305        }
306        target(out);
307        for (_, p) in &self.pseudo {
308            out.push(':');
309            out.push_str(p.name());
310        }
311        if let Some(e) = self.element {
312            out.push_str("::");
313            out.push_str(e.name());
314        }
315    }
316
317    /// Opens the `@media` and `@container` blocks these conditions need, and
318    /// returns how many [`Conditions::close`] has to close.
319    pub(crate) fn open(&self, out: &mut String) -> usize {
320        let mut open = 0;
321        if !self.media.is_empty() {
322            out.push_str("@media ");
323            for (i, m) in self.media.iter().enumerate() {
324                if i > 0 {
325                    out.push_str(" and ");
326                }
327                out.push_str(m.query());
328            }
329            out.push('{');
330            open += 1;
331        }
332        if !self.container.is_empty() {
333            out.push_str("@container ");
334            for (i, c) in self.container.iter().enumerate() {
335                if i > 0 {
336                    out.push_str(" and ");
337                }
338                out.push_str(c.query());
339            }
340            out.push('{');
341            open += 1;
342        }
343        open
344    }
345
346    /// Closes the blocks [`Conditions::open`] opened.
347    pub(crate) fn close(open: usize, out: &mut String) {
348        for _ in 0..open {
349            out.push('}');
350        }
351    }
352}