sickle_ui_scaffold/ui_style/
manual.rs

1use bevy::{ecs::system::EntityCommand, prelude::*, text::TextLayoutInfo, ui::widget::TextFlags};
2
3use crate::{flux_interaction::FluxInteraction, theme::icons::IconData};
4
5use super::{
6    generated::*, LockableStyleAttribute, LockedStyleAttributes, UiStyle, UiStyleUnchecked,
7};
8
9// Special style-related components needing manual implementation
10macro_rules! check_lock {
11    ($world:expr, $entity:expr, $prop:literal, $lock_attr:path) => {
12        if let Some(locked_attrs) = $world.get::<LockedStyleAttributes>($entity) {
13            if locked_attrs.contains($lock_attr) {
14                warn!(
15                    "Failed to style {} property on entity {:?}: Attribute locked!",
16                    $prop, $entity
17                );
18                return;
19            }
20        }
21    };
22}
23
24impl EntityCommand for SetZIndex {
25    fn apply(self, entity: Entity, world: &mut World) {
26        if self.check_lock {
27            check_lock!(world, entity, "z index", LockableStyleAttribute::ZIndex);
28        }
29
30        let Some(mut z_index) = world.get_mut::<ZIndex>(entity) else {
31            warn!(
32                "Failed to set z index on entity {}: No ZIndex component found!",
33                entity
34            );
35            return;
36        };
37
38        // Best effort avoid change triggering
39        if let (ZIndex::Local(level), ZIndex::Local(target)) = (*z_index, self.z_index) {
40            if level != target {
41                *z_index = self.z_index;
42            }
43        } else if let (ZIndex::Global(level), ZIndex::Global(target)) = (*z_index, self.z_index) {
44            if level != target {
45                *z_index = self.z_index;
46            }
47        } else {
48            *z_index = self.z_index;
49        }
50    }
51}
52
53#[derive(Clone, Debug)]
54pub enum ImageSource {
55    Path(String),
56    Lookup(String, fn(String, Entity, &mut World) -> Handle<Image>),
57    Handle(Handle<Image>),
58    Atlas(String, TextureAtlasLayout),
59}
60
61impl Default for ImageSource {
62    fn default() -> Self {
63        Self::Handle(Handle::default())
64    }
65}
66
67impl From<&str> for ImageSource {
68    fn from(path: &str) -> Self {
69        Self::Path(path.to_string())
70    }
71}
72
73impl From<String> for ImageSource {
74    fn from(path: String) -> Self {
75        Self::Path(path)
76    }
77}
78
79pub struct SetImage {
80    source: ImageSource,
81    check_lock: bool,
82}
83
84impl EntityCommand for SetImage {
85    fn apply(self, entity: Entity, world: &mut World) {
86        if self.check_lock {
87            check_lock!(world, entity, "image", LockableStyleAttribute::Image);
88        }
89
90        let handle = match self.source.clone() {
91            ImageSource::Path(path) => {
92                if path == "" {
93                    Handle::default()
94                } else {
95                    world.resource::<AssetServer>().load(path)
96                }
97            }
98            ImageSource::Lookup(path, callback) => callback(path, entity, world),
99            ImageSource::Handle(handle) => handle,
100            ImageSource::Atlas(path, _) => {
101                if path == "" {
102                    Handle::default()
103                } else {
104                    world.resource::<AssetServer>().load(path)
105                }
106            }
107        };
108
109        let Some(mut image) = world.get_mut::<UiImage>(entity) else {
110            warn!(
111                "Failed to set image on entity {}: No UiImage component found!",
112                entity
113            );
114            return;
115        };
116
117        if image.texture != handle {
118            image.texture = handle;
119        }
120
121        if let ImageSource::Atlas(_, layout) = self.source {
122            let layout_handle = world
123                .resource_mut::<Assets<TextureAtlasLayout>>()
124                .add(layout.clone());
125
126            if let Some(mut atlas) = world.get_mut::<TextureAtlas>(entity) {
127                if atlas.layout != layout_handle {
128                    atlas.layout = layout_handle;
129                    atlas.index = 0;
130                }
131            } else {
132                world
133                    .entity_mut(entity)
134                    .insert(TextureAtlas::from(layout_handle));
135            }
136        }
137    }
138}
139
140pub trait SetImageExt {
141    fn image(&mut self, source: ImageSource) -> &mut Self;
142}
143
144impl SetImageExt for UiStyle<'_> {
145    fn image(&mut self, source: ImageSource) -> &mut Self {
146        self.commands.add(SetImage {
147            source,
148            check_lock: true,
149        });
150        self
151    }
152}
153
154pub trait SetImageUncheckedExt {
155    fn image(&mut self, source: ImageSource) -> &mut Self;
156}
157
158impl SetImageUncheckedExt for UiStyleUnchecked<'_> {
159    fn image(&mut self, source: ImageSource) -> &mut Self {
160        self.commands.add(SetImage {
161            source,
162            check_lock: false,
163        });
164        self
165    }
166}
167
168impl EntityCommand for SetImageTint {
169    fn apply(self, entity: Entity, world: &mut World) {
170        if self.check_lock {
171            check_lock!(
172                world,
173                entity,
174                "image tint",
175                LockableStyleAttribute::ImageTint
176            );
177        }
178
179        let Some(mut image) = world.get_mut::<UiImage>(entity) else {
180            warn!(
181                "Failed to set image tint on entity {}: No UiImage component found!",
182                entity
183            );
184            return;
185        };
186
187        if image.color != self.image_tint {
188            image.color = self.image_tint;
189        }
190    }
191}
192
193impl EntityCommand for SetImageFlip {
194    fn apply(self, entity: Entity, world: &mut World) {
195        if self.check_lock {
196            check_lock!(
197                world,
198                entity,
199                "image flip",
200                LockableStyleAttribute::ImageFlip
201            );
202        }
203
204        let Some(mut image) = world.get_mut::<UiImage>(entity) else {
205            warn!(
206                "Failed to set image flip on entity {}: No UiImage component found!",
207                entity
208            );
209            return;
210        };
211
212        if image.flip_x != self.image_flip.x {
213            image.flip_x = self.image_flip.x;
214        }
215
216        if image.flip_y != self.image_flip.y {
217            image.flip_y = self.image_flip.y;
218        }
219    }
220}
221
222impl EntityCommand for SetImageScaleMode {
223    fn apply(self, entity: Entity, world: &mut World) {
224        if self.check_lock {
225            check_lock!(
226                world,
227                entity,
228                "image scale mode",
229                LockableStyleAttribute::ImageScaleMode
230            );
231        }
232
233        if let Some(image_scale_mode) = self.image_scale_mode {
234            if let Some(mut scale_mode) = world.get_mut::<ImageScaleMode>(entity) {
235                *scale_mode = image_scale_mode;
236            } else {
237                world.entity_mut(entity).insert(image_scale_mode);
238            }
239        } else if let Some(_) = world.get::<ImageScaleMode>(entity) {
240            world.entity_mut(entity).remove::<ImageScaleMode>();
241        }
242    }
243}
244
245pub struct SetFluxInteractionEnabled {
246    enabled: bool,
247    check_lock: bool,
248}
249
250impl EntityCommand for SetFluxInteractionEnabled {
251    fn apply(self, entity: Entity, world: &mut World) {
252        if self.check_lock {
253            check_lock!(
254                world,
255                entity,
256                "flux interaction",
257                LockableStyleAttribute::FluxInteraction
258            );
259        }
260
261        let Some(mut flux_interaction) = world.get_mut::<FluxInteraction>(entity) else {
262            warn!(
263                "Failed to set flux interaction on entity {}: No FluxInteraction component found!",
264                entity
265            );
266            return;
267        };
268
269        if self.enabled {
270            if *flux_interaction == FluxInteraction::Disabled {
271                *flux_interaction = FluxInteraction::None;
272            }
273        } else {
274            if *flux_interaction != FluxInteraction::Disabled {
275                *flux_interaction = FluxInteraction::Disabled;
276            }
277        }
278    }
279}
280
281pub trait SetFluxInteractionExt {
282    fn disable_flux_interaction(&mut self) -> &mut Self;
283    fn enable_flux_interaction(&mut self) -> &mut Self;
284    fn flux_interaction_enabled(&mut self, enabled: bool) -> &mut Self;
285}
286
287impl SetFluxInteractionExt for UiStyle<'_> {
288    fn disable_flux_interaction(&mut self) -> &mut Self {
289        self.commands.add(SetFluxInteractionEnabled {
290            enabled: false,
291            check_lock: true,
292        });
293        self
294    }
295
296    fn enable_flux_interaction(&mut self) -> &mut Self {
297        self.commands.add(SetFluxInteractionEnabled {
298            enabled: true,
299            check_lock: true,
300        });
301        self
302    }
303
304    fn flux_interaction_enabled(&mut self, enabled: bool) -> &mut Self {
305        self.commands.add(SetFluxInteractionEnabled {
306            enabled,
307            check_lock: true,
308        });
309        self
310    }
311}
312
313pub trait SetFluxInteractionUncheckedExt {
314    fn disable_flux_interaction(&mut self) -> &mut Self;
315    fn enable_flux_interaction(&mut self) -> &mut Self;
316    fn flux_interaction_enabled(&mut self, enabled: bool) -> &mut Self;
317}
318
319impl SetFluxInteractionUncheckedExt for UiStyleUnchecked<'_> {
320    fn disable_flux_interaction(&mut self) -> &mut Self {
321        self.commands.add(SetFluxInteractionEnabled {
322            enabled: false,
323            check_lock: false,
324        });
325        self
326    }
327
328    fn enable_flux_interaction(&mut self) -> &mut Self {
329        self.commands.add(SetFluxInteractionEnabled {
330            enabled: true,
331            check_lock: false,
332        });
333        self
334    }
335
336    fn flux_interaction_enabled(&mut self, enabled: bool) -> &mut Self {
337        self.commands.add(SetFluxInteractionEnabled {
338            enabled,
339            check_lock: false,
340        });
341        self
342    }
343}
344
345pub trait SetNodeShowHideExt {
346    fn show(&mut self) -> &mut Self;
347    fn hide(&mut self) -> &mut Self;
348    fn render(&mut self, render: bool) -> &mut Self;
349}
350
351impl SetNodeShowHideExt for UiStyle<'_> {
352    fn show(&mut self) -> &mut Self {
353        self.commands
354            .add(SetVisibility {
355                visibility: Visibility::Inherited,
356                check_lock: true,
357            })
358            .add(SetDisplay {
359                display: Display::Flex,
360                check_lock: true,
361            });
362        self
363    }
364
365    fn hide(&mut self) -> &mut Self {
366        self.commands
367            .add(SetVisibility {
368                visibility: Visibility::Hidden,
369                check_lock: true,
370            })
371            .add(SetDisplay {
372                display: Display::None,
373                check_lock: true,
374            });
375        self
376    }
377
378    fn render(&mut self, render: bool) -> &mut Self {
379        if render {
380            self.commands
381                .add(SetVisibility {
382                    visibility: Visibility::Inherited,
383                    check_lock: true,
384                })
385                .add(SetDisplay {
386                    display: Display::Flex,
387                    check_lock: true,
388                });
389        } else {
390            self.commands
391                .add(SetVisibility {
392                    visibility: Visibility::Hidden,
393                    check_lock: true,
394                })
395                .add(SetDisplay {
396                    display: Display::None,
397                    check_lock: true,
398                });
399        }
400
401        self
402    }
403}
404
405pub trait SetNodeShowHideUncheckedExt {
406    fn show(&mut self) -> &mut Self;
407    fn hide(&mut self) -> &mut Self;
408    fn render(&mut self, render: bool) -> &mut Self;
409}
410
411impl SetNodeShowHideUncheckedExt for UiStyleUnchecked<'_> {
412    fn show(&mut self) -> &mut Self {
413        self.commands
414            .add(SetVisibility {
415                visibility: Visibility::Inherited,
416                check_lock: false,
417            })
418            .add(SetDisplay {
419                display: Display::Flex,
420                check_lock: false,
421            });
422        self
423    }
424
425    fn hide(&mut self) -> &mut Self {
426        self.commands
427            .add(SetVisibility {
428                visibility: Visibility::Hidden,
429                check_lock: false,
430            })
431            .add(SetDisplay {
432                display: Display::None,
433
434                check_lock: false,
435            });
436        self
437    }
438
439    fn render(&mut self, render: bool) -> &mut Self {
440        if render {
441            self.commands
442                .add(SetVisibility {
443                    visibility: Visibility::Inherited,
444                    check_lock: false,
445                })
446                .add(SetDisplay {
447                    display: Display::Flex,
448                    check_lock: false,
449                });
450        } else {
451            self.commands
452                .add(SetVisibility {
453                    visibility: Visibility::Hidden,
454                    check_lock: false,
455                })
456                .add(SetDisplay {
457                    display: Display::None,
458                    check_lock: false,
459                });
460        }
461
462        self
463    }
464}
465
466pub struct SetAbsolutePosition {
467    absolute_position: Vec2,
468    check_lock: bool,
469}
470
471impl EntityCommand for SetAbsolutePosition {
472    fn apply(self, entity: Entity, world: &mut World) {
473        if self.check_lock {
474            check_lock!(world, entity, "position: top", LockableStyleAttribute::Top);
475            check_lock!(
476                world,
477                entity,
478                "position: left",
479                LockableStyleAttribute::Left
480            );
481        }
482
483        let offset = if let Some(parent) = world.get::<Parent>(entity) {
484            let Some(parent_node) = world.get::<Node>(parent.get()) else {
485                warn!(
486                    "Failed to set position on entity {}: Parent has no Node component!",
487                    entity
488                );
489                return;
490            };
491
492            let size = parent_node.unrounded_size();
493            let Some(parent_transform) = world.get::<GlobalTransform>(parent.get()) else {
494                warn!(
495                    "Failed to set position on entity {}: Parent has no GlobalTransform component!",
496                    entity
497                );
498                return;
499            };
500
501            parent_transform.translation().truncate() - (size / 2.)
502        } else {
503            Vec2::ZERO
504        };
505
506        let Some(mut style) = world.get_mut::<Style>(entity) else {
507            warn!(
508                "Failed to set position on entity {}: No Style component found!",
509                entity
510            );
511            return;
512        };
513
514        style.top = Val::Px(self.absolute_position.y - offset.y);
515        style.left = Val::Px(self.absolute_position.x - offset.x);
516    }
517}
518
519pub trait SetAbsolutePositionExt {
520    fn absolute_position(&mut self, position: Vec2) -> &mut Self;
521}
522
523impl SetAbsolutePositionExt for UiStyle<'_> {
524    fn absolute_position(&mut self, position: Vec2) -> &mut Self {
525        self.commands.add(SetAbsolutePosition {
526            absolute_position: position,
527            check_lock: true,
528        });
529        self
530    }
531}
532
533pub trait SetAbsolutePositionUncheckedExt {
534    fn absolute_position(&mut self, position: Vec2) -> &mut Self;
535}
536
537impl SetAbsolutePositionUncheckedExt for UiStyleUnchecked<'_> {
538    fn absolute_position(&mut self, position: Vec2) -> &mut Self {
539        self.commands.add(SetAbsolutePosition {
540            absolute_position: position,
541            check_lock: false,
542        });
543        self
544    }
545}
546
547impl EntityCommand for SetIcon {
548    fn apply(self, entity: Entity, world: &mut World) {
549        // TODO: Rework once text/font is in better shape
550        match self.icon {
551            IconData::None => {
552                if self.check_lock {
553                    check_lock!(world, entity, "icon", LockableStyleAttribute::Image);
554                    // TODO: Check lock on text / font once it is available
555                }
556
557                world.entity_mut(entity).remove::<Text>();
558                world.entity_mut(entity).remove::<UiImage>();
559            }
560            IconData::Image(path, color) => {
561                SetImage {
562                    source: ImageSource::Path(path),
563                    check_lock: self.check_lock,
564                }
565                .apply(entity, world);
566                SetImageTint {
567                    image_tint: color,
568                    check_lock: self.check_lock,
569                }
570                .apply(entity, world);
571            }
572            IconData::FontCodepoint(font, codepoint, color, font_size) => {
573                // TODO: Check lock on text / font once it is available
574
575                world
576                    .entity_mut(entity)
577                    .insert(BackgroundColor(Color::NONE));
578
579                world.entity_mut(entity).remove::<UiImage>();
580                let font = world.resource::<AssetServer>().load(font);
581
582                if let Some(mut text) = world.get_mut::<Text>(entity) {
583                    text.sections = vec![TextSection::new(
584                        codepoint,
585                        TextStyle {
586                            font,
587                            font_size,
588                            color,
589                        },
590                    )];
591                } else {
592                    world.entity_mut(entity).insert((
593                        Text::from_section(
594                            codepoint,
595                            TextStyle {
596                                font,
597                                font_size,
598                                color,
599                            },
600                        )
601                        .with_justify(JustifyText::Center)
602                        .with_no_wrap(),
603                        TextLayoutInfo::default(),
604                        TextFlags::default(),
605                    ));
606                }
607            }
608        }
609    }
610}
611
612#[derive(Clone, Debug)]
613pub enum FontSource {
614    Path(String),
615    Handle(Handle<Font>),
616}
617
618impl Default for FontSource {
619    fn default() -> Self {
620        Self::Handle(Handle::default())
621    }
622}
623
624impl From<&str> for FontSource {
625    fn from(path: &str) -> Self {
626        Self::Path(path.to_string())
627    }
628}
629
630impl From<String> for FontSource {
631    fn from(path: String) -> Self {
632        Self::Path(path)
633    }
634}
635
636// TODO: Update these once font / text handling improves
637impl EntityCommand for SetFont {
638    fn apply(self, entity: Entity, world: &mut World) {
639        let font = match self.font {
640            FontSource::Path(path) => world.resource::<AssetServer>().load(path),
641            FontSource::Handle(handle) => handle,
642        };
643
644        let Some(mut text) = world.get_mut::<Text>(entity) else {
645            warn!(
646                "Failed to set font on entity {}: No Text component found!",
647                entity
648            );
649            return;
650        };
651
652        text.sections = text
653            .sections
654            .iter_mut()
655            .map(|section| {
656                section.style.font = font.clone();
657                section.clone()
658            })
659            .collect();
660    }
661}
662
663impl EntityCommand for SetFontSize {
664    fn apply(self, entity: Entity, world: &mut World) {
665        let Some(mut text) = world.get_mut::<Text>(entity) else {
666            warn!(
667                "Failed to set font on entity {}: No Text component found!",
668                entity
669            );
670            return;
671        };
672
673        text.sections = text
674            .sections
675            .iter_mut()
676            .map(|section| {
677                section.style.font_size = self.font_size;
678                section.clone()
679            })
680            .collect();
681    }
682}
683
684impl EntityCommand for SetSizedFont {
685    fn apply(self, entity: Entity, world: &mut World) {
686        let font = world.resource::<AssetServer>().load(self.sized_font.font);
687
688        let Some(mut text) = world.get_mut::<Text>(entity) else {
689            warn!(
690                "Failed to set sized font on entity {}: No Text component found!",
691                entity
692            );
693            return;
694        };
695
696        text.sections = text
697            .sections
698            .iter_mut()
699            .map(|section| {
700                section.style.font = font.clone();
701                section.style.font_size = self.sized_font.size;
702                section.clone()
703            })
704            .collect();
705    }
706}
707
708impl EntityCommand for SetFontColor {
709    fn apply(self, entity: Entity, world: &mut World) {
710        let Some(mut text) = world.get_mut::<Text>(entity) else {
711            warn!(
712                "Failed to set font on entity {}: No Text component found!",
713                entity
714            );
715            return;
716        };
717
718        text.sections = text
719            .sections
720            .iter_mut()
721            .map(|section| {
722                section.style.color = self.font_color;
723                section.clone()
724            })
725            .collect();
726    }
727}
728
729struct SetLockedAttribute {
730    attribute: LockableStyleAttribute,
731    locked: bool,
732}
733
734impl EntityCommand for SetLockedAttribute {
735    fn apply(self, entity: Entity, world: &mut World) {
736        if let Some(mut locked_attributes) = world.get_mut::<LockedStyleAttributes>(entity) {
737            if self.locked {
738                if !locked_attributes.contains(self.attribute) {
739                    locked_attributes.0.insert(self.attribute);
740                }
741            } else {
742                if locked_attributes.contains(self.attribute) {
743                    locked_attributes.0.remove(&self.attribute);
744                }
745            }
746        } else if self.locked {
747            let mut locked_attributes = LockedStyleAttributes::default();
748            locked_attributes.0.insert(self.attribute);
749            world.entity_mut(entity).insert(locked_attributes);
750        }
751    }
752}
753
754pub trait SetLockedAttributeExt {
755    fn lock_attribute(&mut self, attribute: LockableStyleAttribute) -> &mut Self;
756}
757
758impl SetLockedAttributeExt for UiStyle<'_> {
759    fn lock_attribute(&mut self, attribute: LockableStyleAttribute) -> &mut Self {
760        self.commands.add(SetLockedAttribute {
761            attribute,
762            locked: true,
763        });
764        self
765    }
766}
767
768pub trait SetLockedAttributeUncheckedExt {
769    fn unlock_attribute(&mut self, attribute: LockableStyleAttribute) -> &mut Self;
770}
771
772impl SetLockedAttributeUncheckedExt for UiStyleUnchecked<'_> {
773    fn unlock_attribute(&mut self, attribute: LockableStyleAttribute) -> &mut Self {
774        self.commands.add(SetLockedAttribute {
775            attribute,
776            locked: false,
777        });
778        self
779    }
780}
781
782impl EntityCommand for SetScale {
783    fn apply(self, entity: Entity, world: &mut World) {
784        if self.check_lock {
785            check_lock!(world, entity, "scale", LockableStyleAttribute::Scale);
786        }
787
788        let Some(mut transform) = world.get_mut::<Transform>(entity) else {
789            warn!(
790                "Failed to set scale on entity {}: No Transform component found!",
791                entity
792            );
793            return;
794        };
795
796        let new_scale = Vec3::ONE * self.scale;
797        if transform.scale != new_scale {
798            transform.scale = new_scale;
799        }
800    }
801}
802
803impl EntityCommand for SetSize {
804    fn apply(self, entity: Entity, world: &mut World) {
805        if self.check_lock {
806            check_lock!(world, entity, "size: width", LockableStyleAttribute::Width);
807            check_lock!(
808                world,
809                entity,
810                "size: height",
811                LockableStyleAttribute::Height
812            );
813        }
814
815        let Some(mut style) = world.get_mut::<Style>(entity) else {
816            warn!(
817                "Failed to set size on entity {}: No Style component found!",
818                entity
819            );
820            return;
821        };
822
823        if style.width != self.size {
824            style.width = self.size;
825        }
826
827        if style.height != self.size {
828            style.height = self.size;
829        }
830    }
831}