beuvy_runtime/stylesheet/
runtime.rs1use 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}