sickle_ui_scaffold/ui_style/
builder.rs

1use 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    /// Switch context of styling by changing the placement of the DynamicStyle and the target of interaction styling.
179    /// Values are mapped to the UiContext of the themed component. `None` placement refers to the main entity.
180    /// `None` target refers to the current placement entity.
181    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    /// Resets both placement and target to the main entity.
193    pub fn reset_context(&mut self) -> &mut Self {
194        self.placement = None;
195        self.target = None;
196        self
197    }
198
199    /// Revert StyleBuilder to place style on the main entity.
200    pub fn reset_placement(&mut self) -> &mut Self {
201        self.placement = None;
202        self
203    }
204
205    /// Revert StyleBuilder to target the main entity for styling.
206    pub fn reset_target(&mut self) -> &mut Self {
207        self.target = None;
208        self
209    }
210
211    /// All subsequent calls to the StyleBuilder will add styling to the selected sub-component.
212    /// NOTE: The DynamicStyle will be placed on the selected sub-component and interactions will be
213    /// detected on it. This allows styling sub-components directly. It also allows detecting interactions
214    /// on a sub-component and proxying it to the main entity or other sub-components.
215    pub fn switch_placement(&mut self, placement: &'static str) -> &mut Self {
216        self.switch_placement_with(SmolStr::new_static(placement))
217    }
218
219    /// See [`Self::switch_placement`].
220    pub fn switch_placement_with(&mut self, placement: SmolStr) -> &mut Self {
221        self.placement = Some(placement);
222        self
223    }
224
225    /// All subsequent calls to the StyleBuilder will target styling to the selected sub-component.
226    /// NOTE: The DynamicStyle will still be set on the main entity and interactions will be
227    /// detected on it. This allows styling sub-components by proxy from the current placement.
228    pub fn switch_target(&mut self, target: &'static str) -> &mut Self {
229        self.switch_target_with(SmolStr::new_static(target))
230    }
231
232    /// See [`Self::switch_target`].
233    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    /// Converts to `DynamicStyles` using a buffer source for the `DynamicStyle` inner attribute buffer.
259    ///
260    /// This method is potentially non-allocating if the returned buffers have enough capacity and all attributes
261    /// can be cloned without allocating.
262    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    /// Clears the builder without deallocating.
322    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            // Safe unwrap: checked in if above
362            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}