Skip to main content

rosin_core/css/
mod.rs

1//! Types related to styling the UI tree.
2//!
3//! This document assumes familiarity with CSS.
4//!
5//! ## Overview
6//! Stylesheets are typically loaded with the [`stylesheet!()`](crate::stylesheet) macro,
7//! but [`Stylesheet`] also implements [`std::str::FromStr`] and has a [`from_file`](crate::css::Stylesheet::from_file) method.
8//!
9//! In debug builds, stylesheets loaded with [`from_file`](crate::css::Stylesheet::from_file) will be automatically re-loaded when changed.
10//!
11//! Stylesheets are scoped, affecting only the nodes they're attached to and their descendants.
12//! The [`Ui::style_sheet`](crate::tree::Ui::style_sheet) method is used to attach them to nodes.
13//!
14//! The supported properties are mostly standard CSS, with deviations primarily related to layout.
15//! The [`layout`](crate::layout) module documentation explains what those properties do.
16//!
17//! `selection-background` and `selection-color` are the other non-standard properties available.
18//! They control what text looks like when it's selected. Widgets must manually read those values in order to draw selected text correctly.
19//!
20//! Styles can also be changed dynamically at runtime by assigning an [`on_style`](crate::tree::Ui::on_style) callback to a node.
21//!
22//! ## Selectors
23//! The following CSS selectors are supported:
24//!
25//! - Class selectors: `.button`, `.primary`, `.card`, etc.
26//!   - Element selectors are treated as class selectors: `button { ... }` behaves like `.button { ... }`.
27//!
28//! - Wildcard selector: `*`
29//!   - Matches any node.
30//!
31//! - Combinators
32//!   - Descendant: `.panel .title { ... }`
33//!     - Matches a `.title` with any `.panel` ancestors.
34//!   - Child: `.panel > .title { ... }`
35//!     - Matches `.title` only when it is an immediate child of `.panel`.
36//!
37//! - Pseudo-classes
38//!   - `:hover`
39//!   - `:focus`
40//!   - `:active`
41//!   - `:enabled`
42//!   - `:disabled`
43//!
44//! - Grouping
45//!   - Use commas to apply the same properties to multiple selectors: `.btn, .link { ... }`
46//!
47//! Specificity:
48//!   - Standard CSS specificity rules apply for supported selectors.
49//!   - If specificity ties, the rule that appears later in the stylesheet wins.
50//!   - Rules from stylesheets higher in the tree always override rules from stylesheets lower in the tree.
51//!
52//! ## CSS Variables
53//!
54//! - Define variables with `--name: value;` inside any rule.
55//! - Use them with `var(--name)` or `var(--name, fallback)`.
56//!
57//! ```css
58//! .theme {
59//!   --accent: lightblue;
60//!   color: var(--accent);
61//! }
62//!
63//! .danger {
64//!   --accent: orange; /* overrides for this subtree */
65//! }
66//! ```
67//!
68//! ## Supported Properties
69//!
70//! All properties accept `initial | inherit`.
71//!
72//! `box-shadow` parses `inset` but the renderer doesn't support it yet, so inset shadows are currently ignored.
73//!
74//! **Grammar Notation:**
75//!
76//! ```text
77//! A | B = alternatives
78//! A B = sequence, space separated
79//! [ A ] = optional
80//! A? = optional (0 or 1)
81//! A# = one or more, comma separated
82//! A{1,4} = 1 to 4 occurrences
83//! A || B || C = one or more, in any order
84//! ```
85//!
86//! **Grammar Symbols:**
87//!
88//! ```text
89//! <color> = a CSS <color> value (including currentColor).
90//! <integer> = a CSS <integer> token.
91//! <number> = a CSS <number> token.
92//! <percentage> = <number>%
93//! <length> = <number>px | <number>em | 0
94//! <positive-length> = <length> (non-negative)
95//! <unit> = auto | <percentage> | <length> | <stretch>
96//! <positive-unit> = <unit> (non-negative)
97//! <stretch> = <number> | <time>
98//! <time> = <number>s | 0
99//! <angle> =  0 | <number>deg | <number>rad | <number>grad | <number>turn
100//! <angle-deg> = <number>deg
101//!
102//! <font-width> = <percentage> \| normal \| ultra-condensed \| extra-condensed
103//!     \| condensed \| semi-condensed \| semi-expanded \| expanded
104//!     \| extra-expanded \| ultra-expanded
105//!
106//! <stroke-shorthand> =
107//!     [ ( <positive-length> | thin | medium | thick ) || solid || <color> ]
108//!
109//! <shadow> =
110//!     [ inset ]? [ <color> ]? <length>{2,4}
111//!
112//! <text-shadow> =
113//!     [ <color> ]? <length>{2,3}
114//!
115//! <translate-length> =
116//!     0 | <number>px
117//!
118//! <transform-function> =
119//!     translate(<translate-length> [ , ]? [ <translate-length> ]?)
120//!     | rotate(<angle>)
121//!     | scale(<number> [ , ]? [ <number> ]?)
122//!     | skew(<angle> [ , ]? [ <angle> ]?)
123//!     | matrix(<number>#{6})
124//!
125//! <side-or-corner> =
126//!     left | right | top | bottom
127//!     | left top | left bottom | right top | right bottom
128//!
129//! <color-space> =
130//!     srgb | srgb-linear | linear-srgb | display-p3 | a98-rgb | prophoto-rgb
131//!     | rec2020 | lab | lch | hsl | hwb | oklab | oklch | xyz-d50 | xyz | xyz-d65
132//!     | acescg | aces-cg | aces2065-1
133//!
134//! <hue-direction> =
135//!     shorter | longer | increasing | decreasing
136//!
137//! <stop-or-hint> =
138//!     <color> [ <percentage> [ <percentage> ]? ]? | <percentage>
139//!
140//! <color-stop-list> =
141//!     <stop-or-hint> ( , <stop-or-hint> )+
142//!
143//! <linear-gradient> =
144//!     linear-gradient([ <angle> | to <side-or-corner> ]? [ , ]?
145//!       [ in <color-space> [ <hue-direction> hue ]? ]? [ , ]?
146//!       <color-stop-list>
147//!     )
148//!
149//! <font> =
150//!     [ <font-style> \|\| <font-weight> \|\| <font-width> ]?
151//!     <font-size> [ / ( normal \| <unit> ) ]? <family-name>#
152//! ```
153//!
154//! **Properties Table:**
155//!
156//! | Property | Value |
157//! |---|---|
158//! | `background-color` | `<color>` |
159//! | `background-image` | `none \| <linear-gradient>#` |
160//! | `border-bottom-color` | `<color>` |
161//! | `border-bottom-left-radius` | `<positive-length>` |
162//! | `border-bottom-right-radius` | `<positive-length>` |
163//! | `border-bottom-width` | `<positive-length>` |
164//! | `border-bottom` | `<stroke-shorthand>` |
165//! | `border-color` | `<color>{1,4}` |
166//! | `border-left-color` | `<color>` |
167//! | `border-left-width` | `<positive-length>` |
168//! | `border-left` | `<stroke-shorthand>` |
169//! | `border-radius` | `<positive-length>{1,4}` |
170//! | `border-right-color` | `<color>` |
171//! | `border-right-width` | `<positive-length>` |
172//! | `border-right` | `<stroke-shorthand>` |
173//! | `border-top-color` | `<color>` |
174//! | `border-top-left-radius` | `<positive-length>` |
175//! | `border-top-right-radius` | `<positive-length>` |
176//! | `border-top-width` | `<positive-length>` |
177//! | `border-top` | `<stroke-shorthand>` |
178//! | `border-width` | `( <positive-length> \| <line-width-keyword> ){1,4}` |
179//! | `border` | `<stroke-shorthand>` |
180//! | `bottom` | `<unit>` |
181//! | `box-shadow` | `none \| <shadow>#` |
182//! | `child-between` | `<positive-unit>` |
183//! | `child-bottom` | `<positive-unit>` |
184//! | `child-left` | `<positive-unit>` |
185//! | `child-right` | `<positive-unit>` |
186//! | `child-top` | `<positive-unit>` |
187//! | `color` | `<color>` |
188//! | `display` | `none \| row \| row-reverse \| column \| column-reverse` |
189//! | `flex-basis` | `<positive-length>` |
190//! | `font-family` | `<family-name>#` |
191//! | `font-size` | `<number> \| <number>px` |
192//! | `font-style` | `normal \| italic \| oblique [ <angle-deg> ]?` |
193//! | `font-weight` | `normal \| bold \| <number>` |
194//! | `font-width` | `<font-width>` |
195//! | `font` | `<font>` |
196//! | `height` | `<positive-unit>` |
197//! | `left` | `<unit>` |
198//! | `letter-spacing` | `<unit>` |
199//! | `line-height` | `<positive-unit>` |
200//! | `max-bottom` | `<positive-length>` |
201//! | `max-child-between` | `<positive-length>` |
202//! | `max-child-bottom` | `<positive-length>` |
203//! | `max-child-left` | `<positive-length>` |
204//! | `max-child-right` | `<positive-length>` |
205//! | `max-child-top` | `<positive-length>` |
206//! | `max-height` | `<positive-length>` |
207//! | `max-left` | `<positive-length>` |
208//! | `max-right` | `<positive-length>` |
209//! | `max-top` | `<positive-length>` |
210//! | `max-width` | `<positive-length>` |
211//! | `min-bottom` | `<positive-length>` |
212//! | `min-child-between` | `<positive-length>` |
213//! | `min-child-bottom` | `<positive-length>` |
214//! | `min-child-left` | `<positive-length>` |
215//! | `min-child-right` | `<positive-length>` |
216//! | `min-child-top` | `<positive-length>` |
217//! | `min-height` | `<positive-length>` |
218//! | `min-left` | `<positive-length>` |
219//! | `min-right` | `<positive-length>` |
220//! | `min-top` | `<positive-length>` |
221//! | `min-width` | `<positive-length>` |
222//! | `opacity` | `<number> \| <percentage>` |
223//! | `outline-color` | `<color>` |
224//! | `outline-offset` | `<length>` |
225//! | `outline-width` | `<positive-length>` |
226//! | `outline` | `<stroke-shorthand>` |
227//! | `position` | `parent-directed \| self-directed \| fixed` |
228//! | `right` | `<unit>` |
229//! | `selection-background` | `<color>` |
230//! | `selection-color` | `<color>` |
231//! | `space` | `<unit>{1,4}` |
232//! | `text-align` | `start \| end \| left \| right \| center \| justify` |
233//! | `text-shadow` | `none \| <text-shadow>#` |
234//! | `top` | `<unit>` |
235//! | `transform` | `none \| <transform-function>+` |
236//! | `width` | `<positive-unit>` |
237//! | `word-spacing` | `<unit>` |
238//! | `z-index` | `<integer>` |
239
240mod parser;
241mod properties;
242mod style;
243mod stylesheet;
244
245pub use style::*;
246pub use stylesheet::*;
247
248pub(crate) const HOVER_DIRTY: u8 = 1 << 0;
249pub(crate) const FOCUS_DIRTY: u8 = 1 << 1;
250pub(crate) const ACTIVE_DIRTY: u8 = 1 << 2;
251pub(crate) const ENABLED_DIRTY: u8 = 1 << 3;
252
253#[inline]
254pub(crate) fn log_error(msg: impl std::fmt::Display, location: cssparser::SourceLocation, file_name: Option<&std::path::Path>) {
255    if let Some(path) = file_name {
256        log::error!("{msg} {}:{}:{}", path.display(), location.line + 1, location.column);
257    } else {
258        log::error!("{msg} <no-filename>:{}:{}", location.line + 1, location.column);
259    }
260}
261
262/// Loads a CSS [`Stylesheet`] from a path relative to the crate root.
263///
264/// In release builds, uses [`include_str`] to embed the CSS text into the binary.
265#[macro_export]
266macro_rules! stylesheet {
267    ($path:literal) => {{
268        #[cfg(not(debug_assertions))]
269        {
270            use std::str::FromStr;
271            Stylesheet::from_str(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $path))).expect("Failed to parse CSS")
272        }
273
274        #[cfg(debug_assertions)]
275        {
276            let css_path = concat!(env!("CARGO_MANIFEST_DIR"), "/", $path);
277            Stylesheet::from_file(css_path).expect("Failed to parse CSS")
278        }
279    }};
280}