Skip to main content

beuvy_runtime/stylesheet/
runtime.rs

1use super::model::{RuntimeStyleSource, UiStyleSheet};
2use super::parser::{compose_style_sheet, parse_style_sheet};
3use std::fs;
4use std::sync::{Arc, OnceLock, RwLock};
5use std::time::SystemTime;
6
7const DEFAULT_UI_STYLESHEET: &str = concat!(
8    include_str!("../styles/base.css"),
9    "\n",
10    include_str!("../styles/button.css"),
11    "\n",
12    include_str!("../styles/input.css"),
13    "\n",
14    include_str!("../styles/select.css"),
15    "\n",
16    include_str!("../styles/form_item.css"),
17    "\n",
18);
19static RUNTIME_STYLE_SOURCE: OnceLock<RwLock<Arc<RuntimeStyleSource>>> = OnceLock::new();
20static RUNTIME_STYLE_SNAPSHOT: OnceLock<RwLock<Option<RuntimeStyleSheetSnapshot>>> =
21    OnceLock::new();
22
23#[derive(Debug, Clone)]
24struct RuntimeStyleSheetSnapshot {
25    source: RuntimeStyleSource,
26    modified: Option<SystemTime>,
27    sheet: Arc<UiStyleSheet>,
28}
29
30pub fn default_style_sheet() -> &'static UiStyleSheet {
31    static DEFAULT_STYLE_SHEET: OnceLock<UiStyleSheet> = OnceLock::new();
32    DEFAULT_STYLE_SHEET.get_or_init(|| {
33        parse_style_sheet(DEFAULT_UI_STYLESHEET).expect("default ui stylesheet should parse")
34    })
35}
36
37pub fn replace_runtime_style_source(source: RuntimeStyleSource) {
38    let lock = RUNTIME_STYLE_SOURCE.get_or_init(|| RwLock::new(Arc::new(source.clone())));
39    *lock
40        .write()
41        .expect("runtime style source lock should not be poisoned") = Arc::new(source);
42}
43
44pub fn runtime_style_source() -> &'static RuntimeStyleSource {
45    let lock =
46        RUNTIME_STYLE_SOURCE.get_or_init(|| RwLock::new(Arc::new(RuntimeStyleSource::default())));
47    let snapshot = lock
48        .read()
49        .expect("runtime style source lock should not be poisoned");
50    unsafe { &*Arc::as_ptr(&snapshot) }
51}
52
53pub fn runtime_style_sheet() -> Arc<UiStyleSheet> {
54    let source = runtime_style_source().clone();
55    if matches!(source, RuntimeStyleSource::BuiltIn) {
56        return Arc::new(default_style_sheet().clone());
57    }
58    let modified = source_file_modified(&source);
59
60    let lock = RUNTIME_STYLE_SNAPSHOT.get_or_init(|| RwLock::new(None));
61    if let Some(snapshot) = lock
62        .read()
63        .expect("runtime style sheet lock should not be poisoned")
64        .as_ref()
65        && snapshot.source == source
66        && snapshot.modified == modified
67    {
68        return Arc::clone(&snapshot.sheet);
69    }
70
71    let sheet = Arc::new(load_runtime_style_sheet(&source));
72    *lock
73        .write()
74        .expect("runtime style sheet lock should not be poisoned") =
75        Some(RuntimeStyleSheetSnapshot {
76            source,
77            modified,
78            sheet: Arc::clone(&sheet),
79        });
80    sheet
81}
82
83pub(super) fn load_runtime_style_sheet(source: &RuntimeStyleSource) -> UiStyleSheet {
84    let RuntimeStyleSource::File(path) = source else {
85        return default_style_sheet().clone();
86    };
87    let Ok(raw) = fs::read_to_string(path) else {
88        return default_style_sheet().clone();
89    };
90
91    match compose_style_sheet(default_style_sheet(), &raw) {
92        Ok(sheet) => sheet,
93        Err(error) => {
94            bevy::log::warn!(
95                "failed to compose runtime stylesheet {}: {}; falling back to built-in defaults",
96                path,
97                error.reason
98            );
99            default_style_sheet().clone()
100        }
101    }
102}
103
104fn source_file_modified(source: &RuntimeStyleSource) -> Option<SystemTime> {
105    let RuntimeStyleSource::File(path) = source else {
106        return None;
107    };
108    fs::metadata(path)
109        .and_then(|metadata| metadata.modified())
110        .ok()
111}