Skip to main content

traaains/ui/train/
train_window.rs

1use bevy::{
2    ecs::{
3        change_detection::DetectChanges,
4        component::Component,
5        entity::Entity,
6        event::EntityEvent,
7        hierarchy::Children,
8        observer::On,
9        query::With,
10        system::{Commands, Query, Res, ResMut, Single},
11        world::Ref,
12    },
13    feathers::{
14        constants::icons,
15        containers::{
16            flex_spacer, group, group_body, group_header, pane, pane_body, pane_header, subpane,
17            subpane_body, subpane_header,
18        },
19        controls::{
20            ButtonVariant, FeathersButton, FeathersNumberInput, FeathersToolButton, HardLimit,
21            NumberInputValue,
22        },
23        display::{icon, label, label_dim},
24        theme::ThemedText,
25    },
26    picking::events::{PointerEnter, PointerLeave},
27    scene::{CommandsSceneExt, Scene, bsn, on},
28    state::{
29        state::{ComputedStates, NextState, State},
30        state_scoped::DespawnOnExit,
31    },
32    text::TextSpan,
33    ui::{
34        AlignItems, Display, FlexDirection, JustifyContent, Node, PositionType, percent, px,
35        widget::Text,
36    },
37    ui_widgets::{Activate, ValueChange},
38};
39
40use crate::{
41    sim::{
42        Controls, Velocity,
43        train::{Carriage, Train, TrainMember, commands::RemoveTrain},
44    },
45    ui::{EditingWindow, IsHoveringUi, UiState, target::Target},
46};
47
48pub fn carriage_list_item(idx: usize, carriage: (&Carriage, &TrainMember)) -> impl Scene {
49    bsn! {
50        @group() Children [
51            @group_header() Children [ Text(format!("Carriage {}", idx)) ThemedText ]
52            --
53            @group_body() Children [
54                @label("\"Engine\" power:")
55                --
56                @label_dim(format!("{} W", carriage.0.power))
57            ]
58        ]
59    }
60}
61
62#[derive(Clone, Component, Default)]
63pub struct TrainWindow;
64
65#[derive(Clone, Component, Default)]
66pub struct TrainVelocityIndicator;
67
68// only update on velocity change
69pub fn update_vel_indicator(
70    mut velocity_indicator: Single<(&Target, &mut TextSpan), With<TrainVelocityIndicator>>,
71    q_trains: Query<Ref<Velocity>, With<Train>>,
72) {
73    match q_trains.get(velocity_indicator.0.0) {
74        Ok(vel) => {
75            if vel.is_changed() {
76                let mps = vel.0;
77                velocity_indicator.1.0 = (mps * 3.6).to_string();
78            }
79        }
80        Err(_) => {
81            velocity_indicator.1.0 = "-".to_string();
82        }
83    }
84}
85
86pub fn train_window<'a>(
87    train_entity: Entity,
88    controls: &'a Controls,
89    carriages: impl Iterator<Item = (&'a Carriage, &'a TrainMember)>,
90) -> impl Scene {
91    let carriages_items: Vec<_> = carriages
92        .enumerate()
93        .map(|(i, c)| carriage_list_item(i, c))
94        .collect();
95
96    bsn! {
97        TrainWindow
98        EditingWindow
99        Node {
100            position_type: PositionType::Absolute,
101            display: Display::Flex,
102            flex_direction: FlexDirection::Column,
103            align_items: AlignItems::Stretch,
104            justify_content: JustifyContent::Start,
105            padding: px(8),
106            row_gap: px(8),
107            width: percent(30),
108            min_width: px(200),
109        }
110        DespawnOnExit<UiState>(UiState::TrainWindow { target: train_entity })
111        Target(train_entity)
112        Children [
113            @pane() Children [
114                @pane_header() Children [
115                    @label(format!("Train {}", train_entity))
116                    --
117                    @flex_spacer()
118                    --
119                    @FeathersToolButton {
120                        @variant: ButtonVariant::Plain,
121                    }
122                    Children [
123                        @icon(icons::X)
124                    ]
125                    on(|_: On<Activate>, mut next_state: ResMut<NextState<UiState>>| {
126                        next_state.set(UiState::Default);
127                    })
128                ]
129                --
130                @pane_body() Children [
131                    @subpane() Children [
132                        @subpane_header() Children [ Text("Control") ThemedText ]
133                        --
134                        @subpane_body() Children [
135                            @label("Velocity: ")
136                            Children [
137                                TextSpan("")
138                                Target(train_entity)
139                                TrainVelocityIndicator
140                                --
141                                TextSpan(" km/h")
142                            ]
143                            --
144                            @label("Throttle: ")
145                            --
146                            @FeathersNumberInput
147                            NumberInputValue::F32({controls.throttle})
148                            HardLimit::f32(0.0..=1.0)
149                            Target(train_entity)
150                            on(|e: On<ValueChange<f32>>, mut cmd: Commands, q_targets: Query<&Target>, mut q_trains: Query<&mut Controls, With<Train>>| {
151                                let target_train = q_targets.get(e.event_target()).unwrap().0;
152                                let mut train_controls = q_trains.get_mut(target_train).unwrap();
153                                train_controls.throttle = e.value;
154                                cmd.entity(e.event_target()).insert(NumberInputValue::F32(e.value));
155                            })
156                            --
157                            @label("Break: ")
158                            --
159                            @FeathersNumberInput
160                            NumberInputValue::F32({controls.brake})
161                            HardLimit::f32(0.0..=1.0)
162                            Target(train_entity)
163                            on(|e: On<ValueChange<f32>>, mut cmd: Commands, q_targets: Query<&Target>, mut q_trains: Query<&mut Controls, With<Train>>| {
164                                let target_train = q_targets.get(e.event_target()).unwrap().0;
165                                let mut train_controls = q_trains.get_mut(target_train).unwrap();
166                                train_controls.brake = e.value;
167                                cmd.entity(e.event_target()).insert(NumberInputValue::F32(e.value));
168                            })
169                            --
170                            @FeathersButton {
171                                @caption: bsn! { Text("Despawn") ThemedText },
172                            }
173                            Target(train_entity)
174                            on(|e: On<Activate>, mut cmd: Commands, q_targets: Query<&Target>, mut next_state: ResMut<NextState<UiState>>| {
175                                let target_train = q_targets.get(e.event_target()).unwrap().0;
176                                cmd.queue(RemoveTrain::new(target_train));
177                                next_state.set(UiState::Default);
178                            })
179                        ]
180                    ]
181                    --
182                    @subpane() Children [
183                        @subpane_header() Children [ Text("Carriages") ThemedText ]
184                        --
185                        @subpane_body() Children [{carriages_items}]
186                    ]
187                ]
188            ]
189        ]
190        on(|_: On<PointerEnter>, mut next_state: ResMut<NextState<IsHoveringUi>>| {
191            next_state.set(IsHoveringUi(true));
192        })
193        on(|_: On<PointerLeave>, mut next_state: ResMut<NextState<IsHoveringUi>>| {
194            next_state.set(IsHoveringUi(false));
195        })
196    }
197}
198
199#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
200pub struct IsTrainsWindow;
201
202impl ComputedStates for IsTrainsWindow {
203    type SourceStates = UiState;
204
205    fn compute(sources: Self::SourceStates) -> Option<Self> {
206        match sources {
207            UiState::TrainWindow { .. } => Some(IsTrainsWindow),
208            _ => None,
209        }
210    }
211}
212
213pub fn open_trains_window(
214    mut cmd: Commands,
215    q_carriages: Query<(&Carriage, &TrainMember)>,
216    q_trains: Query<(&Train, &Controls)>,
217    curr_state: Res<State<UiState>>,
218) {
219    let UiState::TrainWindow { target } = curr_state.get() else {
220        panic!("`open_trains_window` ran from the wrong state!");
221    };
222
223    if let Ok(train) = q_trains.get(*target) {
224        let carriages = train
225            .0
226            .carriages
227            .iter()
228            .map(|c| q_carriages.get(*c).unwrap());
229
230        cmd.spawn_scene(train_window(*target, train.1, carriages));
231    }
232}