Skip to main content

makara/widgets/
circular.rs

1use bevy::render::render_resource::AsBindGroup;
2use bevy::shader::ShaderRef;
3use bevy::{prelude::*, ui_widgets::observe};
4
5use crate::{consts::*, events::*, utils::*, on_mouse_out};
6use super::*;
7
8/// Material for `circular`.
9#[derive(AsBindGroup, Asset, TypePath, Debug, Clone)]
10pub struct CircularMaterial {
11    #[uniform(0)]
12    u_color: Vec4,
13    #[uniform(1)]
14    u_time: Vec4,
15    #[uniform(2)]
16    u_percent: f32,
17    #[uniform(3)]
18    u_type: f32,
19    #[uniform(4)]
20    u_background_color: Vec4
21}
22
23impl UiMaterial for CircularMaterial {
24    fn fragment_shader() -> ShaderRef {
25        get_embedded_asset_path("embedded_assets/circular.wgsl").into()
26    }
27}
28
29/// Marker component for `circular`.
30#[derive(Component)]
31pub struct MakaraCircular;
32
33/// This color is used in shader file as background ring.
34#[derive(Component)]
35pub struct CircularBackgroundColor(pub Color);
36
37/// Type of circular.
38#[derive(Component)]
39pub enum CircularType {
40    /// A non-stop spinning effect.
41    Indeterminate,
42
43    /// A ring is circled at provided value. 100% means full ring.
44    Percent(f32)
45}
46
47/// Component used to store color of `circular`.
48#[derive(Component)]
49pub struct CircularColor(pub Color);
50
51/// A struct used to mutate components attached to `circular` widget.
52pub struct CircularWidget<'a, 'w, 's> {
53    pub entity: Entity,
54    pub class: &'a mut Class,
55    pub style: WidgetStyle<'a>,
56    pub spin_color: &'a mut CircularColor,
57    pub arc_color: &'a mut CircularBackgroundColor,
58    pub commands: &'a mut Commands<'w, 's>
59}
60
61impl<'a, 'w, 's> CircularWidget<'a, 'w, 's> {
62    /// Turn `circular` into indeterminate mode.
63    pub fn set_indeterminate(&mut self) {
64        self.commands.trigger(SetCircularValue {
65            entity: self.entity,
66            circular_type: "indeterminate".to_string(),
67            value: 0.0
68        });
69    }
70
71    /// Turn `circular` into percentage mode.
72    pub fn set_percentage(&mut self, percent: f32) {
73        self.commands.trigger(SetCircularValue {
74            entity: self.entity,
75            circular_type: "percentage".to_string(),
76            value: percent
77        });
78    }
79}
80
81type IsCircularOnly = (
82    (
83        With<MakaraCircular>,
84        Without<MakaraCheckbox>,
85        Without<MakaraCheckboxButton>,
86        Without<MakaraColumn>,
87        Without<MakaraRow>,
88        Without<MakaraRoot>,
89        Without<MakaraButton>,
90        Without<MakaraDropdown>,
91        Without<MakaraDropdownOverlay>,
92        Without<MakaraImage>,
93        Without<MakaraLink>,
94        Without<MakaraModal>,
95        Without<MakaraModalBackdrop>,
96    ),
97    (
98        Without<MakaraProgressBar>,
99        Without<MakaraRadio>,
100        Without<MakaraRadioGroup>,
101        Without<MakaraScroll>,
102        Without<MakaraScrollbar>,
103        Without<MakaraTextInput>,
104        Without<MakaraTextInputCursor>,
105        Without<MakaraSlider>,
106        Without<MakaraSliderThumb>,
107        Without<MakaraSelect>,
108        Without<MakaraSelectOverlay>,
109    )
110);
111
112/// `circular` system param.
113#[derive(SystemParam)]
114pub struct CircularQuery<'w, 's> {
115    pub id: Query<'w, 's, (Entity, &'static Id), With<MakaraCircular>>,
116    pub class: Query<'w, 's, (Entity, &'static mut Class), IsCircularOnly>,
117    pub style: StyleQuery<'w, 's, IsCircularOnly>,
118    pub custom_style: Query<
119        'w, 's,
120        (&'static mut CircularColor, &'static mut CircularBackgroundColor)
121    >,
122    pub commands: Commands<'w, 's>
123}
124
125impl<'w, 's> WidgetQuery<'w, 's> for CircularQuery<'w, 's> {
126    type WidgetView<'a> = CircularWidget<'a, 'w, 's> where Self: 'a;
127
128    fn get_components<'a>(&'a mut self, entity: Entity) -> Option<Self::WidgetView<'a>> {
129        let CircularQuery { id: _, class, style, custom_style, commands } = self;
130
131        let style_bundle = style.query.get_mut(entity).ok()?;
132        let (node, bg, border_color, shadow, z_index) = style_bundle;
133
134        let custom_style = custom_style.get_mut(entity).ok()?;
135        let (cir_color, cir_bg_color) = custom_style;
136
137        return Some(CircularWidget {
138            entity,
139            class: class.get_mut(entity).ok()?.1.into_inner(),
140            style: WidgetStyle {
141                node: node.into_inner(),
142                background_color: bg.into_inner(),
143                border_color: border_color.into_inner(),
144                shadow: shadow.into_inner(),
145                z_index: z_index.into_inner(),
146            },
147            spin_color: cir_color.into_inner(),
148            arc_color: cir_bg_color.into_inner(),
149            commands: commands
150        });
151    }
152
153    fn find_by_id<'a>(&'a mut self, target_id: &str) -> Option<Self::WidgetView<'a>> {
154        let entity = self.id.iter()
155            .find(|(_, id)| id.0 == target_id)
156            .map(|(e, _)| e)?;
157
158        self.get_components(entity)
159    }
160
161    fn find_by_entity<'a>(&'a mut self, entity: Entity) -> Option<Self::WidgetView<'a>> {
162        self.get_components(entity)
163    }
164
165    fn find_by_class(&self, target_class: &str) -> Vec<Entity> {
166        self.class.iter()
167            .filter(|(_, class)| class.0.split(" ").any(|word| word == target_class))
168            .map(|(e, _)| e)
169            .collect()
170    }
171}
172
173/// Bundle for creating `circular`.
174#[derive(Bundle)]
175pub struct CircularBundle {
176    pub id_class: IdAndClass,
177    pub style: ContainerStyle,
178    pub circular_type: CircularType,
179    pub circular_color: CircularColor,
180    pub tooltip_bundle: TooltipBundle
181}
182
183impl Default for CircularBundle {
184    fn default() -> Self {
185        let style = ContainerStyle {
186            node: Node {
187                width: px(50),
188                height: px(50),
189                justify_content: JustifyContent::Center,
190                align_items: AlignItems::Center,
191                border: UiRect::all(Val::Px(0.0)),
192                ..default()
193            },
194            shadow: BoxShadow::default(),
195            ..default()
196        };
197
198        let circular_type = CircularType::Indeterminate;
199        let circular_color = CircularColor(DEFAULT_CIRCULAR_VALUE_COLOR);
200        let tooltip_bundle = TooltipBundle::default();
201        let id_class = IdAndClass::default();
202
203        Self { style, circular_type, circular_color, tooltip_bundle, id_class }
204    }
205}
206
207impl CircularBundle {
208    /// Set percentage for circular.
209    ///
210    /// If this method is called after mode, it will override the circular type.
211    pub fn percent(mut self, percent: f32) -> Self {
212        self.circular_type = CircularType::Percent(percent);
213        self
214    }
215
216    /// Set mode for circular, either "indeterminate" or "percentage".
217    /// Default is "indeterminate".
218    pub fn mode(mut self, mode: &str) -> Self {
219        if mode.trim() == "indeterminate" {
220            self.circular_type = CircularType::Indeterminate
221        }
222        else {
223            self.circular_type = CircularType::Percent(0.0);
224        }
225        self
226    }
227
228    /// Set circular spinning color. Default color will be used if this method is not called.
229    pub fn color(mut self, color: impl IntoColor) -> Self {
230        self.circular_color.0 = color.into_color();
231        self
232    }
233}
234
235impl Widget for CircularBundle {
236    /// Build `circular`.
237    fn build(mut self) -> impl Bundle {
238        process_built_in_spacing_class(&self.id_class.class, &mut self.style.node);
239        process_built_in_color(&self.id_class.class, &mut self.circular_color.0);
240
241        (
242            self.id_class,
243            self.style,
244            self.circular_color,
245            self.circular_type,
246            MakaraCircular,
247            MakaraWidget,
248            CircularBackgroundColor(LIGHT_CIRCULAR_BG_COLOR),
249            observe(on_circular_value_set),
250            observe(on_circular_mouse_over),
251            observe(on_mouse_out),
252            children![
253                self.tooltip_bundle.build()
254            ]
255        )
256    }
257}
258
259impl SetContainerStyle for CircularBundle {
260    fn container_style(&mut self) -> &mut ContainerStyle {
261        &mut self.style
262    }
263}
264
265impl SetToolTip for CircularBundle {
266    fn set_tooltip(&mut self) -> &mut TooltipBundle {
267        &mut self.tooltip_bundle
268    }
269}
270
271impl SetIdAndClass for CircularBundle {
272    fn id_and_class(&mut self) -> &mut IdAndClass {
273        &mut self.id_class
274    }
275}
276
277/// Create default circular (light variant) as default theme is light.
278pub fn circular() -> CircularBundle {
279    let bundle = CircularBundle::default();
280    bundle
281}
282
283pub(crate) fn detect_circular_class_change_for_built_in_color(
284    mut circulars: Query<(&Class, &mut Node, &mut CircularColor), IsCircularOnly>
285) {
286    for (class, mut node, mut cir_color) in circulars.iter_mut() {
287        process_built_in_spacing_class(class, &mut node);
288        process_built_in_color(class, &mut cir_color.0);
289    }
290}
291
292fn on_circular_value_set(
293    value_set: On<SetCircularValue>,
294    mut circulars: Query<&mut CircularType>,
295    mut commands: Commands
296) {
297    if let Ok(mut cir_type) = circulars.get_mut(value_set.entity) {
298        match value_set.circular_type.as_str() {
299            "indeterminate" => *cir_type = CircularType::Indeterminate,
300            "percentage" => {
301                *cir_type = CircularType::Percent(value_set.value.clamp(0.0, 100.0));
302                commands.trigger(Change {
303                    entity: value_set.entity,
304                    data: value_set.value.clamp(0.0, 100.0)
305                });
306            }
307            _ => {}
308        }
309    }
310}
311
312fn on_circular_mouse_over(
313    mut over: On<Pointer<Over>>,
314    mut commands: Commands,
315    mut tooltips: Query<
316        (&mut Node, &ComputedNode, &TooltipPosition, &UseTooltip),
317        With<MakaraTooltip>
318    >,
319    circulars: Query<
320        (&Children, &UiTransform, &ComputedNode),
321        With<MakaraCircular>
322    >,
323) {
324    if let Ok((children, transform, computed)) = circulars.get(over.entity) {
325        show_or_hide_tooltip(true, &mut tooltips, Some(computed), Some(transform), children);
326    }
327    commands.trigger(MouseOver {
328        entity: over.entity,
329    });
330    over.propagate(false);
331}
332
333pub(crate) fn detect_circular_added(
334    mut commands: Commands,
335    mut circular_materials: ResMut<Assets<CircularMaterial>>,
336    circulars: Query<
337        (Entity, &CircularType, &CircularColor, &CircularBackgroundColor),
338        Or<(Added<MakaraCircular>, Changed<ComputedNode>)>
339    >
340) {
341    for (entity, circular_type, color, bg_color) in circulars.iter() {
342        if let Color::Srgba(value) = color.0 {
343            let (u_type, u_percent) = match circular_type {
344                CircularType::Indeterminate => (0.0, 0.0),
345                CircularType::Percent(percent) =>  (1.0, *percent),
346            };
347
348            if let Color::Srgba(bg_value) = bg_color.0 {
349                commands
350                    .entity(entity)
351                    .insert(
352                        MaterialNode(circular_materials.add(CircularMaterial {
353                            u_time: Vec4::ZERO,
354                            u_color: Vec4::new(value.red, value.green, value.blue, 1.0),
355                            u_background_color: Vec4::new(
356                                bg_value.red,
357                                bg_value.green,
358                                bg_value.blue,
359                                bg_value.alpha
360                            ),
361                            u_type,
362                            u_percent
363                        }))
364                    );
365            }
366        }
367    }
368}
369
370pub(crate) fn update_circular_material_u_time(
371    time: Res<Time>,
372    mut materials: ResMut<Assets<CircularMaterial>>,
373    query: Query<(&MaterialNode<CircularMaterial>, &CircularType, &CircularColor, &CircularBackgroundColor)>
374) {
375    query.iter().for_each(|(handle, circular_type, color, bg_color)| {
376        if let Some(material) = materials.get_mut(handle) {
377            match circular_type {
378                CircularType::Indeterminate => {
379                    material.u_time.x = -time.elapsed_secs();
380                    material.u_type = 0.0;
381                }
382                CircularType::Percent(value) => {
383                    material.u_time.x = 0.0;
384                    material.u_percent = *value;
385                    material.u_type = 1.0;
386                }
387            }
388            if let Color::Srgba(value) = bg_color.0 {
389                material.u_background_color = Vec4::new(
390                    value.red, value.green, value.blue, value.alpha
391                );
392            }
393
394            if let Color::Srgba(value) = color.0 {
395                material.u_color = Vec4::new(
396                    value.red, value.green, value.blue, value.alpha
397                );
398            }
399        }
400    });
401}
402
403pub(crate) fn update_circular_style_on_theme_change_system(
404    makara_theme: Res<MakaraTheme>,
405    mut circulars: Query<(&mut CircularBackgroundColor, &CircularType), With<MakaraCircular>>,
406) {
407    if !makara_theme.is_changed() {
408        return;
409    }
410
411    let new_bg_color = match makara_theme.theme {
412        Theme::Light => LIGHT_CIRCULAR_BG_COLOR,
413        Theme::Dark => DARK_CIRCULAR_BG_COLOR,
414    };
415
416    for (mut bg_color, cir_type) in circulars.iter_mut() {
417        match cir_type {
418            CircularType::Percent(_) => {
419                // only react to theme change if color is default
420                if bg_color.0 == LIGHT_CIRCULAR_BG_COLOR ||
421                   bg_color.0 == DARK_CIRCULAR_BG_COLOR
422                {
423                    bg_color.0 = new_bg_color;
424                }
425            },
426            _ => {}
427        }
428
429    }
430}
431
432pub(crate) fn can_run_circular_systems(q: Query<&MakaraCircular>) -> bool {
433    q.count() > 0
434}