sickle_ui_scaffold/ui_style/
builder.rs1use bevy::prelude::*;
2use smol_str::SmolStr;
3
4use crate::{prelude::FluxInteraction, theme::prelude::*};
5
6use super::{
7 attribute::{
8 CustomAnimatedStyleAttribute, CustomInteractiveStyleAttribute, CustomStaticStyleAttribute,
9 },
10 generated::*,
11 LogicalEq,
12};
13
14pub struct InteractiveStyleBuilder<'a> {
15 pub style_builder: &'a mut StyleBuilder,
16}
17
18impl<'a> InteractiveStyleBuilder<'a> {
19 pub fn custom(
20 &mut self,
21 callback: impl Fn(Entity, FluxInteraction, &mut World) + Send + Sync + 'static,
22 ) -> &mut Self {
23 self.style_builder.add(DynamicStyleAttribute::Interactive(
24 InteractiveStyleAttribute::Custom(CustomInteractiveStyleAttribute::new(callback)),
25 ));
26
27 self
28 }
29}
30
31pub struct AnimatedStyleBuilder<'a> {
32 pub style_builder: &'a mut StyleBuilder,
33}
34
35impl AnimatedStyleBuilder<'_> {
36 pub fn add_and_extract_animation(
37 &mut self,
38 attribute: DynamicStyleAttribute,
39 ) -> &mut AnimationSettings {
40 let index = self.style_builder.add(attribute.clone());
41
42 let DynamicStyleAttribute::Animated {
43 controller: DynamicStyleController {
44 ref mut animation, ..
45 },
46 ..
47 } = self.style_builder.attributes[index].attribute
48 else {
49 unreachable!();
50 };
51
52 animation
53 }
54
55 pub fn custom(
56 &mut self,
57 callback: impl Fn(Entity, AnimationState, &mut World) + Send + Sync + 'static,
58 ) -> &mut AnimationSettings {
59 let attribute = DynamicStyleAttribute::Animated {
60 attribute: AnimatedStyleAttribute::Custom(CustomAnimatedStyleAttribute::new(callback)),
61 controller: DynamicStyleController::default(),
62 };
63
64 self.add_and_extract_animation(attribute)
65 }
66}
67
68#[derive(Clone, Debug)]
69pub struct ContextStyleAttributeConfig {
70 placement: Option<SmolStr>,
71 target: Option<SmolStr>,
72 attribute: DynamicStyleAttribute,
73}
74
75impl LogicalEq for ContextStyleAttributeConfig {
76 fn logical_eq(&self, other: &Self) -> bool {
77 self.placement == other.placement
78 && self.target == other.target
79 && self.attribute.logical_eq(&other.attribute)
80 }
81}
82
83#[derive(Default, Debug)]
84pub struct StyleBuilder {
85 placement: Option<SmolStr>,
86 target: Option<SmolStr>,
87 attributes: Vec<ContextStyleAttributeConfig>,
88}
89
90impl From<StyleBuilder> for DynamicStyle {
91 fn from(value: StyleBuilder) -> Self {
92 value.attributes.iter().for_each(|attr| {
93 if attr.placement.is_some() || attr.target.is_some() {
94 warn!(
95 "StyleBuilder with context-bound attributes converted without context! \
96 Some attributes discarded! \
97 This can be the result of using `PseudoTheme::build()` and calling \
98 `style_builder.switch_placement(CONTEXT)` in the callback, which is not supported.",
99 );
100 }
101 });
102
103 DynamicStyle::new(
104 value
105 .attributes
106 .iter()
107 .filter(|attr| attr.placement.is_none() || attr.target.is_none())
108 .map(|attr| attr.attribute.clone())
109 .collect(),
110 )
111 }
112}
113
114impl StyleBuilder {
115 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn new_with_capacity(num_attributes: usize) -> Self {
120 Self {
121 placement: None,
122 target: None,
123 attributes: Vec::with_capacity(num_attributes),
124 }
125 }
126
127 pub fn add(&mut self, attribute: DynamicStyleAttribute) -> usize {
128 let index = self.attributes.iter().position(|csac| {
129 csac.placement == self.placement
130 && csac.target == self.target
131 && csac.attribute.logical_eq(&attribute)
132 });
133
134 match index {
135 Some(index) => {
136 warn!(
137 "Overwriting {:?} with {:?}",
138 self.attributes[index], attribute
139 );
140 self.attributes[index].attribute = attribute;
141
142 index
143 }
144 None => {
145 self.attributes.push(ContextStyleAttributeConfig {
146 placement: self.placement.clone(),
147 target: self.target.clone(),
148 attribute,
149 });
150 self.attributes.len() - 1
151 }
152 }
153 }
154
155 pub fn custom(
156 &mut self,
157 callback: impl Fn(Entity, &mut World) + Send + Sync + 'static,
158 ) -> &mut Self {
159 self.add(DynamicStyleAttribute::Static(StaticStyleAttribute::Custom(
160 CustomStaticStyleAttribute::new(callback),
161 )));
162
163 self
164 }
165
166 pub fn interactive(&mut self) -> InteractiveStyleBuilder {
167 InteractiveStyleBuilder {
168 style_builder: self,
169 }
170 }
171
172 pub fn animated(&mut self) -> AnimatedStyleBuilder {
173 AnimatedStyleBuilder {
174 style_builder: self,
175 }
176 }
177
178 pub fn switch_context(
182 &mut self,
183 placement: impl Into<Option<&'static str>>,
184 target: impl Into<Option<&'static str>>,
185 ) -> &mut Self {
186 self.placement = placement.into().map(|p| SmolStr::new_static(p));
187 self.target = target.into().map(|p| SmolStr::new_static(p));
188
189 self
190 }
191
192 pub fn reset_context(&mut self) -> &mut Self {
194 self.placement = None;
195 self.target = None;
196 self
197 }
198
199 pub fn reset_placement(&mut self) -> &mut Self {
201 self.placement = None;
202 self
203 }
204
205 pub fn reset_target(&mut self) -> &mut Self {
207 self.target = None;
208 self
209 }
210
211 pub fn switch_placement(&mut self, placement: &'static str) -> &mut Self {
216 self.switch_placement_with(SmolStr::new_static(placement))
217 }
218
219 pub fn switch_placement_with(&mut self, placement: SmolStr) -> &mut Self {
221 self.placement = Some(placement);
222 self
223 }
224
225 pub fn switch_target(&mut self, target: &'static str) -> &mut Self {
229 self.switch_target_with(SmolStr::new_static(target))
230 }
231
232 pub fn switch_target_with(&mut self, target: SmolStr) -> &mut Self {
234 self.target = Some(target);
235 self
236 }
237
238 pub fn convert_with(mut self, context: &impl UiContext) -> Vec<(Option<Entity>, DynamicStyle)> {
239 self.attributes
240 .sort_unstable_by(|a, b| a.placement.cmp(&b.placement));
241 let count = self
242 .attributes
243 .chunk_by(|a, b| a.placement == b.placement)
244 .count();
245
246 let mut result: Vec<(Option<Entity>, DynamicStyle)> = Vec::with_capacity(count);
247 result.extend(self.convert_to_iter(context));
248 result
249 }
250
251 pub fn convert_to_iter<'a>(
252 &'a mut self,
253 context: &'a impl UiContext,
254 ) -> impl Iterator<Item = (Option<Entity>, DynamicStyle)> + 'a {
255 self.convert_to_iter_with_buffers(context, Vec::default)
256 }
257
258 pub fn convert_to_iter_with_buffers<'a>(
263 &'a mut self,
264 context: &'a impl UiContext,
265 buffer_source: impl FnMut() -> Vec<ContextStyleAttribute> + 'a,
266 ) -> impl Iterator<Item = (Option<Entity>, DynamicStyle)> + 'a {
267 self.attributes
268 .sort_unstable_by(|a, b| a.placement.cmp(&b.placement));
269
270 self.attributes
271 .chunk_by(|a, b| a.placement == b.placement)
272 .scan(0, |index, placement_chunk| {
273 let start = *index;
274 let end = start + placement_chunk.len();
275 let placement = placement_chunk[0].placement.clone();
276 *index = end;
277 Some((start, end, placement))
278 })
279 .filter_map(|(start, end, placement)| {
280 let mut placement_entity: Option<Entity> = None;
281
282 if let Some(target_placement) = placement {
283 let target_entity = match context.get(target_placement.as_str()) {
284 Ok(entity) => entity,
285 Err(msg) => {
286 warn!("{}", msg);
287 return None;
288 }
289 };
290
291 if target_entity == Entity::PLACEHOLDER {
292 #[cfg(not(feature = "disable-ui-context-placeholder-warn"))]
293 warn!("Entity::PLACEHOLDER returned for placement target!");
294
295 return None;
296 } else {
297 placement_entity = Some(target_entity);
298 }
299 }
300
301 Some((start, end, placement_entity))
302 })
303 .scan(
304 buffer_source,
305 |buffer_source, (start, end, placement_entity)| {
306 let mut attributes = (buffer_source)();
307 attributes.clear();
308 Some((
309 placement_entity,
310 DynamicStyle::copy_from(self.attributes[start..end].iter().fold(
311 attributes,
312 |acc: Vec<ContextStyleAttribute>, csac| {
313 StyleBuilder::fold_context_style_attributes(acc, csac, context)
314 },
315 )),
316 ))
317 },
318 )
319 }
320
321 pub fn clear(&mut self) {
323 self.target = None;
324 self.placement = None;
325 self.attributes.clear();
326 }
327
328 fn fold_context_style_attributes(
329 mut acc: Vec<ContextStyleAttribute>,
330 csac: &ContextStyleAttributeConfig,
331 context: &impl UiContext,
332 ) -> Vec<ContextStyleAttribute> {
333 let new_entry: ContextStyleAttribute = match &csac.target {
334 Some(target) => match context.get(target) {
335 Ok(target_entity) => match target_entity == Entity::PLACEHOLDER {
336 true => {
337 #[cfg(not(feature = "disable-ui-context-placeholder-warn"))]
338 warn!("Entity::PLACEHOLDER returned for styling target!");
339
340 return acc;
341 }
342 false => {
343 ContextStyleAttribute::new(target_entity, csac.attribute.clone()).into()
344 }
345 },
346 Err(msg) => {
347 warn!("{}", msg);
348 return acc;
349 }
350 },
351 None => ContextStyleAttribute::new(None, csac.attribute.clone()).into(),
352 };
353
354 if !acc
355 .iter()
356 .any(|csa: &ContextStyleAttribute| csa.logical_eq(&new_entry))
357 {
358 acc.push(new_entry);
359 } else {
360 warn!("Style overwritten for {:?}", new_entry);
361 let index = acc
363 .iter()
364 .position(|csa| csa.logical_eq(&new_entry))
365 .unwrap();
366 acc[index] = new_entry;
367 }
368
369 acc
370 }
371}