Skip to main content

traaains/ui/track/segment/signals_list/
item.rs

1use bevy::{
2    ecs::{
3        change_detection::DetectChanges,
4        component::Component,
5        entity::Entity,
6        hierarchy::{ChildOf, Children},
7        observer::On,
8        query::With,
9        system::{Commands, Query},
10        world::Ref,
11    },
12    feathers::{
13        constants::icons,
14        containers::{flex_spacer, group, group_body, group_header},
15        controls::{ButtonVariant, FeathersToolButton},
16        display::{icon, label, label_small},
17    },
18    scene::{CommandsSceneExt, Scene, bsn, on},
19    ui_widgets::Activate,
20};
21
22use crate::{
23    sim::{
24        signal::{Signals, commands::RemoveSignal},
25        track::pos::TrackPos,
26    },
27    ui::{
28        Target,
29        segment::signals_list::item::{
30            data::{SignalData, signal_data, signal_data_main, signal_data_stop},
31            index_input::SignalIndexInput,
32            type_input::signal_type_input,
33        },
34    },
35};
36
37pub mod data;
38pub mod index_input;
39pub mod type_input;
40
41// Contemplating whether or not to change the whole approach here. On one hand, having each of the inputs store
42// a reference to the target signal might seem wasteful. On the other hand, it would meang splitting the one update
43// system into a bunch of other smaller systems, which might be better in the long run.
44//
45// With this approach, one simply needs to check whether the target signal still exists and if not, delete the item
46// from the single update function, which might prove difficult if we split the system. On the other hand, we need
47// to send an event from the input observers to despawn the item anyway, since they don't have a direct connection
48// (could have tho!)
49
50#[derive(Component, Clone, Default)]
51pub struct SignalListItem;
52
53pub fn signals_list_item(
54    idx: usize,
55    entity: Entity,
56    variant: &Signals,
57    track_pos: &TrackPos,
58) -> impl Scene {
59    bsn! {
60        @group()
61        SignalListItem
62        Target(entity)
63        Children [
64            @group_header() Children [
65                @label(format!("Segment #{}", idx + 1))
66                --
67                @flex_spacer()
68                --
69                @FeathersToolButton {
70                    @variant: ButtonVariant::Plain,
71                }
72                Children [
73                    @icon(icons::X)
74                ]
75                on(|e: On<Activate>, mut cmd: Commands, target: Query<(Entity, &Target), With<SignalListItem>>,
76                children: Query<&ChildOf>| {
77                    let (signal_list_item, signal) = find_root_with_target(e.entity, &target, &children).unwrap();
78                    cmd.queue(RemoveSignal::new(signal));
79                    cmd.entity(signal_list_item).despawn();
80                })
81            ]
82            --
83            @group_body() Children [
84                @label_small("Track index")
85                --
86                @SignalIndexInput {
87                    @value: {track_pos.idx}
88                }
89                Target(entity)
90                --
91                @label_small("Signal type")
92                --
93                @signal_type_input(variant.into())
94                --
95                @label_small("Signal data")
96                --
97                @signal_data(variant)
98            ]
99        ]
100    }
101}
102
103/// Find parent with `Target` component and returns the target entity along with the target signal entity.
104fn find_root_with_target(
105    curr_entity: Entity,
106    target: &Query<(Entity, &Target), With<SignalListItem>>,
107    children: &Query<&ChildOf>,
108) -> Option<(Entity, Entity)> {
109    if let Ok((te, t)) = target.get(curr_entity) {
110        return Some((te, t.0));
111    }
112
113    if let Ok(child) = children.get(curr_entity) {
114        return find_root_with_target(child.parent(), target, children);
115    }
116
117    None
118}
119
120fn find_signal_data(
121    curr_entity: Entity,
122    signal_data: &Query<Entity, With<SignalData>>,
123    children: &Query<&Children>,
124) -> Option<Entity> {
125    if let Ok(e) = signal_data.get(curr_entity) {
126        return Some(e);
127    }
128
129    // the current entity can have no children, thus we check
130    if let Ok(curr_children) = children.get(curr_entity) {
131        for c in curr_children {
132            if let Some(e) = find_signal_data(*c, signal_data, children) {
133                return Some(e);
134            }
135        }
136    }
137
138    None
139}
140
141/// System that checks for updates on values of signals and updates the UI
142pub fn update_signal_list_items(
143    mut cmd: Commands,
144    signal_list_items: Query<(Entity, &Target), With<SignalListItem>>,
145    _signal_index_inputs: Query<Entity, With<SignalIndexInput>>,
146    signal_data: Query<Entity, With<SignalData>>,
147    signals: Query<(Ref<Signals>, Ref<TrackPos>)>,
148    children: Query<&Children>,
149) {
150    for (item_e, item_target) in signal_list_items {
151        match signals.get(item_target.0) {
152            Ok((signal_var, _signal_track_pos)) => {
153                if signal_var.is_changed()
154                    && let Some(signal_data) = find_signal_data(item_e, &signal_data, &children)
155                {
156                    cmd.entity(signal_data).despawn_children();
157                    match &*signal_var {
158                        Signals::Main(main_signal) => {
159                            let sd_id = cmd.spawn_scene(signal_data_main(main_signal)).id();
160                            cmd.entity(sd_id).insert(ChildOf(signal_data));
161                        }
162                        Signals::Stop => {
163                            let sd_id = cmd.spawn_scene(signal_data_stop()).id();
164                            cmd.entity(sd_id).insert(ChildOf(signal_data));
165                        }
166                    };
167                }
168            }
169            Err(_) => {
170                // if we failed to get this signal, then it is likely that that signal for whatever reason no longer exists and thus we should
171                // despawn this portion of the ui
172                cmd.entity(item_e).despawn();
173            }
174        }
175    }
176}