famiq/widgets/fps/
mod.rs

1pub mod components;
2pub mod helper;
3pub mod tests;
4
5use crate::utils::*;
6use crate::widgets::*;
7use crate::widgets::text::base_text::*;
8use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
9use bevy::prelude::*;
10
11pub(crate) use components::*;
12pub(crate) use helper::*;
13use famiq_macros::set_widget_attributes;
14
15use super::color::{GREEN_COLOR, WHITE_COLOR, WARNING_COLOR, DANGER_COLOR};
16
17const DEFAULT_FPS_TEXT_SIZE: f32 = 18.0;
18
19#[set_widget_attributes]
20#[derive(Clone, Debug)]
21pub struct FpsBuilder {
22    pub change_color: RVal,
23    pub right_side: RVal,
24    pub all_reactive_keys: Vec<String>,
25    pub root_node: Entity,
26    pub count_text_entity: Option<Entity>
27}
28
29impl FpsBuilder {
30    pub fn new(root_node: Entity, font_handle: &Handle<Font>) -> Self {
31        let mut attributes = WidgetAttributes::default();
32        attributes.font_handle = Some(font_handle.clone());
33        Self {
34            attributes,
35            root_node,
36            all_reactive_keys: Vec::new(),
37            cloned_attrs: WidgetAttributes::default(),
38            change_color: RVal::Bool(true),
39            right_side: RVal::Bool(false),
40            count_text_entity: None
41        }
42    }
43
44    pub fn build_fps_count_text(&mut self, commands: &mut Commands) -> Entity {
45        let text_font = TextFont {
46            font: self.cloned_attrs.font_handle.clone().unwrap(),
47            font_size: DEFAULT_FPS_TEXT_SIZE,
48            ..default()
49        };
50        let entity = commands
51            .spawn((
52                TextSpan::default(),
53                text_font.clone(),
54                TextColor(WHITE_COLOR),
55                IsFPSTextCount,
56                DefaultTextSpanConfig::new(
57                    TextSpan::default(),
58                    text_font,
59                    TextColor(WHITE_COLOR)
60                )
61            ))
62            .id();
63        insert_class_id(commands, entity, &self.cloned_attrs.id, &self.cloned_attrs.class);
64        self.count_text_entity = Some(entity);
65        entity
66    }
67
68    pub fn rebuild_count_text(&mut self, world: &mut World) {
69        insert_class_id_world(
70            world,
71            self.count_text_entity.unwrap(),
72            &self.cloned_attrs.id,
73            &self.cloned_attrs.class
74        );
75    }
76
77    /// Internal system to update the FPS count and optionally change its color based on the value.
78    pub(crate) fn update_fps_count_system(
79        diagnostics: Res<DiagnosticsStore>,
80        mut text_q: Query<(&mut TextSpan, &mut TextColor, &CanChangeColor, &IsFPSTextCount)>
81    ) {
82        for (mut text, mut color, change_color, _) in text_q.iter_mut() {
83            if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
84                if let Some(value) = fps.smoothed() {
85                    let value = value as usize;
86                    text.0 = format!("{value}");
87
88                    if change_color.0 {
89                        if value >= 100 {
90                            color.0 = GREEN_COLOR;
91                        }
92                        else if value >= 60 && value < 100 {
93                            color.0 = WARNING_COLOR;
94                        }
95                        else {
96                            color.0 = DANGER_COLOR;
97                        }
98                    }
99                }
100            }
101        }
102    }
103
104    pub fn set_node_right_side(&mut self, state: bool) {
105        if state {
106            self.cloned_attrs.node.left = Val::Auto;
107            self.cloned_attrs.node.right = Val::Px(6.0);
108        } else {
109            self.cloned_attrs.node.right = Val::Auto;
110            self.cloned_attrs.node.left = Val::Px(6.0);
111        }
112    }
113
114    pub(crate) fn handle_side_val(&mut self, r_data: &HashMap<String, RVal>) {
115        match self.right_side.to_owned() {
116            RVal::Bool(v) => self.set_node_right_side(v),
117            RVal::Str(v) => {
118                let reactive_keys = get_reactive_key(&v);
119                for key in reactive_keys.iter() {
120                    if let Some(r_v) = r_data.get(key) {
121                        match r_v {
122                            RVal::Bool(state) => self.set_node_right_side(*state),
123                            _ => {}
124                        }
125                    }
126                }
127                self.all_reactive_keys.extend_from_slice(&reactive_keys);
128            }
129            _ => {}
130        }
131    }
132
133    pub(crate) fn prepar_attrs(&mut self, r_data: &HashMap<String, RVal>) {
134        self.cloned_attrs = self.attributes.clone();
135        self.cloned_attrs.node = default_fps_container_node();
136        self.cloned_attrs.override_text_size = Some(DEFAULT_FPS_TEXT_SIZE);
137        self.cloned_attrs.default_visibility = Visibility::Visible;
138        self.handle_side_val(r_data);
139        replace_reactive_keys_common_attrs(&mut self.cloned_attrs, r_data, &mut self.all_reactive_keys);
140    }
141}
142
143impl SetupWidget for FpsBuilder {
144    fn components(&mut self) -> impl Bundle {
145        let mut style_components = BaseStyleComponents::default();
146        style_components.node = self.cloned_attrs.node.clone();
147        self._process_built_in_spacing_class();
148        (
149            MainWidget,
150            IsFPSTextLabel,
151            GlobalZIndex(6),
152            style_components.clone(),
153            DefaultWidgetConfig::from(style_components),
154            ReactiveWidget
155        )
156    }
157
158    fn build(&mut self, r_data: &HashMap<String, RVal>, commands: &mut Commands) -> Entity {
159        self.prepar_attrs(r_data);
160        let mut label = FaBaseText::new_with_attributes("FPS:", &self.cloned_attrs);
161        label.use_get_color = true;
162
163        let label_entity = label.build(r_data, commands);
164        let count_entity = self.build_fps_count_text(commands);
165
166        match self.change_color.to_owned() {
167            RVal::Bool(v) => {
168                commands.entity(count_entity).insert(CanChangeColor(v));
169            }
170            RVal::Str(v) => {
171                let reactive_keys = get_reactive_key(&v);
172                for key in reactive_keys.iter() {
173                    if let Some(r_v) = r_data.get(key) {
174                        match r_v {
175                            RVal::Bool(state) => {
176                                commands.entity(count_entity).insert(CanChangeColor(*state));
177                            }
178                            _ => {}
179                        }
180                    }
181                }
182                self.all_reactive_keys.extend_from_slice(&reactive_keys);
183            }
184            _ => {}
185        }
186
187        commands.entity(label_entity).add_child(count_entity).insert(self.components());
188        commands.entity(self.root_node).add_child(label_entity);
189        insert_class_id(commands, label_entity, &self.attributes.id, &self.attributes.class);
190
191        let cloned_builder = self.clone();
192        let ar_keys = self.all_reactive_keys.clone();
193        commands.queue(move |w: &mut World| {
194            w.send_event(UpdateReactiveSubscriberEvent::new(
195                ar_keys,
196                label_entity,
197                WidgetBuilder {
198                    builder: BuilderType::Fps(cloned_builder)
199                }
200            ));
201        });
202        self.all_reactive_keys.clear();
203        label_entity
204    }
205
206    fn rebuild(&mut self, r_data: &HashMap<String, RVal>, old_entity: Entity, world: &mut World) {
207        self.prepar_attrs(r_data);
208        let mut label = FaBaseText::new_with_attributes("FPS:", &self.cloned_attrs);
209        label.use_get_color = true;
210        label.rebuild(r_data, old_entity, world);
211        self.rebuild_count_text(world);
212
213        match self.change_color.to_owned() {
214            RVal::Bool(v) => {
215                world.entity_mut(self.count_text_entity.unwrap()).insert(CanChangeColor(v));
216            }
217            RVal::Str(v) => {
218                let reactive_keys = get_reactive_key(&v);
219                for key in reactive_keys.iter() {
220                    if let Some(r_v) = r_data.get(key) {
221                        match r_v {
222                            RVal::Bool(state) => {
223                                world.entity_mut(self.count_text_entity.unwrap()).insert(CanChangeColor(*state));
224                            }
225                            _ => {}
226                        }
227                    }
228                }
229                self.all_reactive_keys.extend_from_slice(&reactive_keys);
230            }
231            _ => {}
232        }
233        world.entity_mut(old_entity).insert(self.components());
234        insert_class_id_world(world, old_entity, &self.attributes.id, &self.attributes.class);
235
236        let cloned_builder = self.clone();
237        let ar_keys = self.all_reactive_keys.clone();
238        world.send_event(UpdateReactiveSubscriberEvent::new(
239            ar_keys,
240            old_entity,
241            WidgetBuilder {
242                builder: BuilderType::Fps(cloned_builder)
243            }
244        ));
245        self.all_reactive_keys.clear();
246    }
247}
248
249/// Macro for creating fps count.
250#[macro_export]
251macro_rules! fps {
252    ( $( $key:ident : $value:tt ),* $(,)? ) => {{
253        let famiq_builder = builder_mut();
254        let root_entity = famiq_builder.resource.root_node_entity.unwrap();
255        let f_builder = &mut FpsBuilder::new(root_entity, &famiq_builder.get_font_handle());
256        $(
257            $crate::fps_text_attributes!(f_builder, $key : $value);
258        )*
259        f_builder.build(
260            &famiq_builder.reactive_data.data,
261            &mut famiq_builder.ui_root_node.commands()
262        )
263    }};
264}
265
266#[macro_export]
267macro_rules! fps_text_attributes {
268    ($f_builder:ident, change_color: $change_color:expr) => {{
269        match to_rval($change_color) {
270            Ok(v) => $f_builder.change_color = v,
271            Err(_) => panic!("\nchange_color attribute accepts only bool and reactive string\n")
272        }
273    }};
274    ($f_builder:ident, right_side: $right_side:expr) => {{
275        match to_rval($right_side) {
276            Ok(v) => $f_builder.right_side = v,
277            Err(_) => panic!("\right_side attribute accepts only bool and reactive string\n")
278        }
279    }};
280    ($f_builder:ident, $key:ident : $value:expr) => {{
281        $crate::common_attributes!($f_builder, $key : $value);
282    }};
283}
284
285/// a system to check if FPS internal system(s) can run.
286///
287/// True only if fps widget is created.
288pub fn can_run_fps_systems(fps_q: Query<&IsFPSTextLabel>) -> bool {
289    !fps_q.is_empty()
290}