use crate::style::css::{CssDeclaration, CssParser, ParsedCssRule};
use crate::style::css_trace::TracedRuleSheet;
use crate::style::selector::{with_match_scratch, ElementRef, RuleIndex};
use crate::style::PseudoState;
use crate::style::WidgetStyle;
use crate::compat::Mutex;
use crate::compat::MutexGuard;
use crate::compat::OnceLock;
use crate::compat::{lock, MiniToString, String, Vec};
#[derive(Debug)]
struct RegisteredSheet {
name: String,
priority: u8,
rules: Vec<ParsedCssRule>,
index: RuleIndex,
parse_error: Option<String>,
}
#[derive(Debug)]
pub struct StyleSheetManager {
sheets: Vec<RegisteredSheet>,
}
impl StyleSheetManager {
pub fn new() -> Self {
Self { sheets: Vec::new() }
}
pub fn register(&mut self, name: &str, css: &str, priority: u8) {
self.sheets.retain(|s| s.name != name);
let (rules, parse_error) = match CssParser::parse_prepared_rules(css) {
Ok(rules) => (rules.into_iter().filter(|rule| rule.selector.is_some()).collect(), None),
Err(error) => (Vec::new(), Some(error)),
};
let mut index = RuleIndex::new();
for (position, rule) in rules.iter().enumerate() {
if let Some(selector) = &rule.selector {
index.insert(position, selector.index_key());
}
}
self.sheets.push(RegisteredSheet {
name: name.to_string(),
priority,
rules,
index,
parse_error,
});
self.sheets.sort_by_key(|sheet| sheet.priority);
}
pub fn unregister(&mut self, name: &str) -> bool {
let len_before = self.sheets.len();
self.sheets.retain(|s| s.name != name);
self.sheets.len() < len_before
}
pub fn apply_to(
&self,
kind: &str,
class: Option<&str>,
id: Option<&str>,
state: Option<PseudoState>,
style: &mut WidgetStyle,
) -> Result<(), String> {
self.apply_to_with_chain(kind, class, id, state, &[], style)
}
pub fn apply_to_context(
&self,
element: ElementRef<'_>,
ancestors: &[ElementRef<'_>],
style: &mut WidgetStyle,
) -> Result<(), String> {
self.apply_to_with_chain(
element.kind.css_name(),
element.class,
element.id,
element.state,
ancestors,
style,
)
}
pub fn apply_to_context_with_inline(
&self,
element: ElementRef<'_>,
ancestors: &[ElementRef<'_>],
inline_css: Option<&str>,
style: &mut WidgetStyle,
) -> Result<(), String> {
self.apply_to_context(element, ancestors, style)?;
if let Some(text) = inline_css {
CssParser::apply_declaration_text(text, style)?;
}
Ok(())
}
fn apply_to_with_chain(
&self,
kind: &str,
class: Option<&str>,
id: Option<&str>,
state: Option<PseudoState>,
ancestors: &[ElementRef<'_>],
style: &mut WidgetStyle,
) -> Result<(), String> {
let mut parse_error: Option<String> = None;
let mut matched: Vec<(u32, usize, usize, &[CssDeclaration])> = Vec::new();
for (sheet_order, sheet) in self.sheets.iter().enumerate() {
if let Some(error) = &sheet.parse_error {
if parse_error.is_none() {
parse_error = Some(error.clone());
}
continue;
}
with_match_scratch(|scratch| {
sheet.index.candidates_into(kind, class, id, scratch);
for &position in scratch.positions.iter() {
let Some(rule) = sheet.rules.get(position) else {
continue;
};
let Some(selector) = &rule.selector else {
continue;
};
if selector.matches_in(kind, class, id, state, ancestors) {
matched.push((rule.specificity, sheet_order, position, &rule.declarations));
}
}
});
}
matched.sort_by_key(|(specificity, sheet_order, position, _)| {
(*specificity, *sheet_order, *position)
});
for (_, _, _, declarations) in matched {
CssParser::apply_declarations(declarations, style)?;
}
if let Some(error) = parse_error {
return Err(error);
}
Ok(())
}
pub fn clear(&mut self) {
self.sheets.clear();
}
pub fn traced_sheets(&self) -> Vec<TracedRuleSheet<'_>> {
self.sheets
.iter()
.map(|sheet| TracedRuleSheet {
name: &sheet.name,
priority: sheet.priority,
rules: &sheet.rules,
})
.collect()
}
pub fn len(&self) -> usize {
self.sheets.len()
}
pub fn is_empty(&self) -> bool {
self.sheets.is_empty()
}
}
crate::impl_default_via_new!(StyleSheetManager);
pub fn global_stylesheet_manager() -> MutexGuard<'static, StyleSheetManager> {
static MANAGER: OnceLock<Mutex<StyleSheetManager>> = OnceLock::new();
lock(MANAGER.get_or_init(|| Mutex::new(StyleSheetManager::new())))
}
#[cfg(test)]
pub(crate) fn stylesheet_test_guard() -> crate::compat::MutexGuard<'static, ()> {
static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
lock(GUARD.get_or_init(|| Mutex::new(())))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Color;
fn make_style() -> WidgetStyle {
WidgetStyle::default()
}
#[test]
fn stylesheet_register_and_apply() {
let mut manager = StyleSheetManager::new();
let css = r#"
Button {
background-color: #ff0000;
text-color: #ffffff;
}
"#;
manager.register("test-sheet", css, 0);
let mut style = make_style();
manager.apply_to("Button", None, None, None, &mut style).expect("apply_to should succeed");
assert_eq!(
style.background_color,
Some(Color::rgb(255, 0, 0)),
"background_color should be red"
);
assert_eq!(style.text_color, Some(Color::rgb(255, 255, 255)), "text_color should be white");
}
#[test]
fn stylesheet_priority() {
let mut manager = StyleSheetManager::new();
let css_low = r#"
Button {
background-color: #ff0000;
text-color: #000000;
}
"#;
manager.register("low", css_low, 0);
let css_high = r#"
Button {
background-color: #0000ff;
}
"#;
manager.register("high", css_high, 100);
let mut style = make_style();
manager.apply_to("Button", None, None, None, &mut style).expect("apply_to should succeed");
assert_eq!(
style.background_color,
Some(Color::rgb(0, 0, 255)),
"high-priority blue should override low-priority red"
);
assert_eq!(
style.text_color,
Some(Color::rgb(0, 0, 0)),
"text_color from low-priority sheet should be preserved"
);
}
#[test]
fn stylesheet_unregister() {
let mut manager = StyleSheetManager::new();
manager.register("theme", "Button { background-color: #00ff00; }", 0);
assert!(!manager.is_empty(), "manager should not be empty after register");
let removed = manager.unregister("theme");
assert!(removed, "unregister should return true when sheet exists");
assert!(manager.is_empty(), "manager should be empty after unregister");
let removed_again = manager.unregister("nonexistent");
assert!(!removed_again, "unregister should return false for unknown name");
}
#[test]
fn stylesheet_clear() {
let mut manager = StyleSheetManager::new();
manager.register("a", "Button { background-color: #ff0000; }", 0);
manager.register("b", "Label { text-color: #00ff00; }", 1);
assert_eq!(manager.len(), 2, "should have 2 sheets");
manager.clear();
assert_eq!(manager.len(), 0, "should be empty after clear");
assert!(manager.is_empty(), "is_empty should be true");
}
#[test]
fn global_manager_thread_safe() {
let _guard = stylesheet_test_guard();
{
let mut mgr = global_stylesheet_manager();
mgr.clear();
mgr.register("global-test", "Button { border-color: #ff00ff; }", 0);
}
{
let mut mgr = global_stylesheet_manager();
let removed = mgr.unregister("global-test");
assert!(removed, "should find the sheet registered in previous scope");
assert!(mgr.is_empty(), "manager should be clean after unregister");
}
}
#[test]
fn stylesheet_register_replaces_existing() {
let mut manager = StyleSheetManager::new();
manager.register("sheet", "Button { background-color: #ff0000; }", 0);
manager.register("sheet", "Button { background-color: #00ff00; }", 0);
assert_eq!(manager.len(), 1, "re-registering same name should replace, not add");
let mut style = make_style();
manager.apply_to("Button", None, None, None, &mut style).expect("apply_to should succeed");
assert_eq!(
style.background_color,
Some(Color::rgb(0, 255, 0)),
"should use the latest registered CSS for the same name"
);
}
#[test]
fn stylesheet_class_selector_match() {
let mut manager = StyleSheetManager::new();
let css = r#"
.primary {
background-color: #ff8800;
}
"#;
manager.register("class-test", css, 0);
let mut style = make_style();
manager
.apply_to("Button", Some("primary"), None, None, &mut style)
.expect("apply_to should succeed");
assert_eq!(
style.background_color,
Some(Color::rgb(0xff, 0x88, 0x00)),
"class selector .primary should match"
);
}
#[test]
fn stylesheet_no_match_does_not_apply() {
let mut manager = StyleSheetManager::new();
let css = r#"
Label {
background-color: #ff0000;
}
"#;
manager.register("no-match", css, 0);
let mut style = make_style();
manager.apply_to("Button", None, None, None, &mut style).expect("apply_to should succeed");
assert_eq!(style.background_color, None, "Label CSS should not apply to Button");
}
#[test]
fn a_broken_sheet_does_not_discard_the_valid_sheets() {
let mut manager = StyleSheetManager::new();
manager.register("bad", "Button { background-color: #ff0000; ", 0);
manager.register("good", "Button { text-color: #123456; }", 1);
let mut style = make_style();
let result = manager.apply_to("Button", None, None, None, &mut style);
assert!(result.is_err(), "the malformed sheet must still be reported");
assert_eq!(
style.text_color,
Some(Color::rgb(0x12, 0x34, 0x56)),
"the valid sheet must still apply"
);
}
#[test]
fn a_class_rule_overrides_a_kind_rule_written_after_it() {
let mut manager = StyleSheetManager::new();
manager.register(
"cascade",
".primary { background-color: #ff8800; } Button { background-color: #0000ff; }",
0,
);
let mut style = make_style();
manager
.apply_to("Button", Some("primary"), None, None, &mut style)
.expect("apply_to should succeed");
assert_eq!(
style.background_color,
Some(Color::rgb(0xff, 0x88, 0x00)),
"`.primary` has higher specificity than `Button` and must win"
);
}
#[test]
fn stylesheet_invalid_css_returns_error() {
let mut manager = StyleSheetManager::new();
manager.register("bad", "Button { background-color: #ff0000; ", 0);
let mut style = make_style();
let result = manager.apply_to("Button", None, None, None, &mut style);
assert!(result.is_err(), "invalid CSS should produce an error");
}
#[test]
fn a_descendant_rule_needs_the_ancestor_chain() {
use crate::style::selector::ElementRef;
use crate::widget::WidgetKind;
let mut manager = StyleSheetManager::new();
manager.register("nav", "Panel Button { background-color: #010203; }", 0);
manager.register("direct", "Panel > Button { text-color: #040506; }", 0);
let button = ElementRef { kind: WidgetKind::Button, class: None, id: None, state: None };
let panel = ElementRef { kind: WidgetKind::Panel, ..button };
let label = ElementRef { kind: WidgetKind::Label, ..button };
let mut style = make_style();
manager
.apply_to_context(button, &[panel, label], &mut style)
.expect("apply_to_context should succeed");
assert_eq!(
style.background_color,
Some(Color::rgb(1, 2, 3)),
"the panel is an ancestor at depth 2"
);
assert_eq!(
style.text_color, None,
"the child combinator must not match with a label between panel and button"
);
let mut immediate = make_style();
manager
.apply_to_context(button, &[panel], &mut immediate)
.expect("apply_to_context should succeed");
assert_eq!(
immediate.text_color,
Some(Color::rgb(4, 5, 6)),
"with the panel as the immediate parent the child rule applies"
);
let mut chain_less = make_style();
manager
.apply_to("Button", None, None, None, &mut chain_less)
.expect("apply_to should succeed");
assert_eq!(
chain_less.background_color, None,
"without a chain a combinator rule matches nothing"
);
}
#[test]
fn inline_declarations_override_the_sheet() {
use crate::style::selector::ElementRef;
use crate::widget::WidgetKind;
let mut manager = StyleSheetManager::new();
manager.register("id-rule", "#main { background-color: #ff0000; }", 0);
let element =
ElementRef { kind: WidgetKind::Button, class: None, id: Some("main"), state: None };
let mut style = make_style();
manager
.apply_to_context_with_inline(
element,
&[],
Some("background-color: #0000ff;"),
&mut style,
)
.expect("inline apply should succeed");
assert_eq!(
style.background_color,
Some(Color::rgb(0, 0, 255)),
"the element's own declaration is the last layer"
);
}
#[test]
fn the_registered_index_never_hides_a_matching_rule() {
let mut manager = StyleSheetManager::new();
manager.register(
"index",
"* { opacity: 0.5; } Button.primary { border-width: 3px; } \
#go { border-radius: 4px; } Button:hover { border-width: 5px; }",
0,
);
let mut style = make_style();
manager
.apply_to("Button", Some("primary"), Some("go"), None, &mut style)
.expect("apply should succeed");
assert_eq!(style.opacity, Some(0.5), "the universal rule is filed under `Any`");
assert_eq!(style.border_width, Some(3), "the compound is filed under its class");
assert_eq!(style.border_radius, Some(4), "the id rule is filed under its id");
let mut hovered = make_style();
manager
.apply_to(
"Button",
Some("primary"),
Some("go"),
Some(crate::style::PseudoState::Hover),
&mut hovered,
)
.expect("apply should succeed");
assert_eq!(
hovered.border_width,
Some(3),
"the state rule is filed where the element can find it; the compound still outranks it"
);
}
#[test]
fn traced_sheets_trace_the_rules_the_apply_uses() {
use crate::style::css_trace::trace_cascade_prepared;
let mut manager = StyleSheetManager::new();
manager.register("s", ".primary { color: #222222; } Button { color: #111111; }", 0);
let sheets = manager.traced_sheets();
assert_eq!(sheets.len(), 1);
let trace = trace_cascade_prepared("Button", Some("primary"), None, None, &sheets);
assert_eq!(
trace.origin_of("color").map(|origin| origin.value.as_str()),
Some("#222222"),
"the class rule is more specific, and it is written first"
);
let mut applied = make_style();
manager
.apply_to("Button", Some("primary"), None, None, &mut applied)
.expect("apply should succeed");
assert_eq!(applied.text_color, Some(Color::rgba(0x22, 0x22, 0x22, 255)));
}
}