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
68pub 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}