1use bevy::ecs::query::QueryFilter;
2use bevy::picking::hover::Hovered;
3use bevy::prelude::*;
4use bevy::ui_widgets::{CoreSliderDragState, SliderRange, SliderValue};
5use bevy_panorbit_camera::PanOrbitCamera;
6
7use crate::components::{
8 MainSceneCamera, PlaybackFrameText, PlaybackPanelRoot, PlaybackPlayPauseButton,
9 PlaybackPlayPauseIcon, PlaybackScrubberButton, PlaybackScrubberFill, PlaybackScrubberThumb,
10 PlaybackSpeedButton, PlaybackSpeedText, PlaybackTitleText,
11};
12use crate::viewer::{MarqueeSelectionState, ViewerState};
13
14use super::state::PlaybackState;
15use crate::ui::{BUTTON_ACTIVE_BACKGROUND, BUTTON_BACKGROUND, BUTTON_HOVER_BACKGROUND};
16
17pub fn sync_playback_state(viewer: Res<ViewerState>, mut playback: ResMut<PlaybackState>) {
18 let next_total = viewer.trajectory_len();
19 playback.current_frame = viewer.current;
20 playback.total_frames = next_total;
21 playback.follow_tail = viewer.follow_tail;
22
23 if next_total <= 1 && playback.is_playing {
24 playback.is_playing = false;
25 }
26}
27
28pub fn sync_playback_visibility(
29 playback: Res<PlaybackState>,
30 mut roots: Query<&mut Node, With<PlaybackPanelRoot>>,
31) {
32 let display = if playback.total_frames <= 1 {
33 Display::None
34 } else {
35 Display::Flex
36 };
37
38 for mut node in &mut roots {
39 node.display = display;
40 }
41}
42
43pub fn sync_playback_camera(
44 mut commands: Commands,
45 main_camera: Query<Entity, With<MainSceneCamera>>,
46 roots: Query<(Entity, Option<&UiTargetCamera>), With<PlaybackPanelRoot>>,
47) {
48 let Ok(main_camera) = main_camera.single() else {
49 return;
50 };
51
52 for (entity, target_camera) in roots.iter() {
53 if target_camera.is_none() {
54 commands.entity(entity).insert(UiTargetCamera(main_camera));
55 }
56 }
57}
58
59pub fn sync_playback_text(
60 mut commands: Commands,
61 playback: Res<PlaybackState>,
62 mut title_texts: Query<
63 &mut Text,
64 (
65 With<PlaybackTitleText>,
66 Without<PlaybackFrameText>,
67 Without<PlaybackSpeedText>,
68 Without<PlaybackPlayPauseIcon>,
69 ),
70 >,
71 mut frame_texts: Query<
72 &mut Text,
73 (
74 With<PlaybackFrameText>,
75 Without<PlaybackTitleText>,
76 Without<PlaybackSpeedText>,
77 Without<PlaybackPlayPauseIcon>,
78 ),
79 >,
80 mut speed_texts: Query<
81 &mut Text,
82 (
83 With<PlaybackSpeedText>,
84 Without<PlaybackTitleText>,
85 Without<PlaybackFrameText>,
86 Without<PlaybackPlayPauseIcon>,
87 ),
88 >,
89 mut play_pause_icons: Query<
90 &mut Text,
91 (
92 With<PlaybackPlayPauseIcon>,
93 Without<PlaybackTitleText>,
94 Without<PlaybackFrameText>,
95 Without<PlaybackSpeedText>,
96 ),
97 >,
98 mut scrubber_fill: Query<
99 &mut Node,
100 (With<PlaybackScrubberFill>, Without<PlaybackScrubberThumb>),
101 >,
102 mut scrubber_thumb: Query<
103 &mut Node,
104 (With<PlaybackScrubberThumb>, Without<PlaybackScrubberFill>),
105 >,
106 slider: Query<
107 (
108 Entity,
109 &SliderValue,
110 &SliderRange,
111 &Hovered,
112 &CoreSliderDragState,
113 ),
114 With<PlaybackScrubberButton>,
115 >,
116 button_children: Query<&Children>,
117 mut button_texts: Query<
118 &mut Text,
119 (
120 Without<PlaybackTitleText>,
121 Without<PlaybackFrameText>,
122 Without<PlaybackSpeedText>,
123 Without<PlaybackPlayPauseIcon>,
124 ),
125 >,
126 play_buttons: Query<
127 (Entity, &Interaction, &mut BackgroundColor),
128 (With<Button>, With<PlaybackPlayPauseButton>),
129 >,
130 speed_buttons: Query<
131 (
132 Entity,
133 &PlaybackSpeedButton,
134 &Interaction,
135 &mut BackgroundColor,
136 ),
137 (With<Button>, Without<PlaybackPlayPauseButton>),
138 >,
139) {
140 if let Ok(mut text) = title_texts.single_mut() {
141 *text = Text::new("Trajectory");
142 }
143 if let Ok(mut text) = frame_texts.single_mut() {
144 *text = Text::new(format!(
145 "Frame {} / {}",
146 playback.current_frame.saturating_add(1),
147 playback.total_frames.max(1)
148 ));
149 }
150 if let Ok(mut text) = speed_texts.single_mut() {
151 *text = Text::new(format!("{} playback", playback.current_speed().label));
152 }
153 if let Ok(mut icon) = play_pause_icons.single_mut() {
154 *icon = Text::new(if playback.is_playing { "⏸" } else { "▶" });
155 }
156
157 if let Ok((entity, slider_value, slider_range, _hovered, drag_state)) = slider.single() {
158 let max = playback.total_frames.saturating_sub(1) as f32;
159 let expected_range = SliderRange::new(0.0, max.max(0.0));
160 if *slider_range != expected_range {
161 commands.entity(entity).insert(expected_range);
162 }
163 let expected_value = playback.current_frame as f32;
164 if !drag_state.dragging && (slider_value.0 - expected_value).abs() > f32::EPSILON {
165 commands.entity(entity).insert(SliderValue(expected_value));
166 }
167
168 let fraction = slider_range.thumb_position(slider_value.0);
169 if let Ok(mut fill) = scrubber_fill.single_mut() {
170 fill.width = percent(fraction * 100.0);
171 }
172 if let Ok(mut thumb) = scrubber_thumb.single_mut() {
173 thumb.left = percent(fraction * 100.0);
174 }
175 }
176
177 for (entity, interaction, mut background) in play_buttons {
178 *background = button_background(playback.is_playing, *interaction);
179 let _ = entity;
180 }
181
182 for (entity, speed, interaction, mut background) in speed_buttons {
183 let is_active = speed.index == playback.speed_index;
184 *background = button_background(is_active, *interaction);
185 if let Some(preset) = PlaybackState::speed_presets().get(speed.index) {
186 update_button_label(entity, &button_children, &mut button_texts, preset.label);
187 }
188 }
189}
190
191pub fn handle_playback_buttons(
192 mut interactions: ParamSet<(
193 Query<
194 &Interaction,
195 (
196 Changed<Interaction>,
197 With<Button>,
198 With<PlaybackPlayPauseButton>,
199 ),
200 >,
201 Query<
202 &Interaction,
203 (
204 Changed<Interaction>,
205 With<Button>,
206 With<crate::components::PlaybackStepBackButton>,
207 Without<PlaybackPlayPauseButton>,
208 ),
209 >,
210 Query<
211 &Interaction,
212 (
213 Changed<Interaction>,
214 With<Button>,
215 With<crate::components::PlaybackStepForwardButton>,
216 Without<PlaybackPlayPauseButton>,
217 Without<crate::components::PlaybackStepBackButton>,
218 ),
219 >,
220 Query<
221 (&Interaction, &PlaybackSpeedButton),
222 (
223 Changed<Interaction>,
224 With<Button>,
225 Without<PlaybackPlayPauseButton>,
226 Without<crate::components::PlaybackStepBackButton>,
227 Without<crate::components::PlaybackStepForwardButton>,
228 ),
229 >,
230 )>,
231 mut viewer: ResMut<ViewerState>,
232 mut playback: ResMut<PlaybackState>,
233) {
234 if interactions
235 .p0()
236 .iter()
237 .any(|interaction| *interaction == Interaction::Pressed)
238 {
239 playback.is_playing = !playback.is_playing;
240 }
241
242 if interactions
243 .p1()
244 .iter()
245 .any(|interaction| *interaction == Interaction::Pressed)
246 {
247 viewer.step_frame(-1);
248 }
249
250 if interactions
251 .p2()
252 .iter()
253 .any(|interaction| *interaction == Interaction::Pressed)
254 {
255 let advanced = viewer.step_frame(1);
256 if !advanced && !viewer.follow_tail {
257 playback.is_playing = false;
258 }
259 }
260
261 for (interaction, speed) in interactions.p3().iter() {
262 if *interaction == Interaction::Pressed {
263 playback.set_speed_index(speed.index);
264 }
265 }
266}
267
268pub fn sync_playback_slider_value(
269 slider: Query<&SliderValue, (Changed<SliderValue>, With<PlaybackScrubberButton>)>,
270 mut viewer: ResMut<ViewerState>,
271) {
272 let Ok(value) = slider.single() else {
273 return;
274 };
275 let max = viewer.trajectory_len().saturating_sub(1) as f32;
276 let frame = value.0.round().clamp(0.0, max) as usize;
277 viewer.set_current_frame_index(frame);
278}
279
280pub fn set_camera_input_enabled(
281 sliders: Query<&CoreSliderDragState, With<PlaybackScrubberButton>>,
282 marquee: Option<Res<MarqueeSelectionState>>,
283 mut cameras: Query<&mut PanOrbitCamera>,
284) {
285 let dragging = sliders.iter().any(|drag| drag.dragging);
286 let marquee_active = marquee
287 .as_ref()
288 .is_some_and(|marquee| marquee.is_tracking());
289 for mut camera in &mut cameras {
290 camera.enabled = !(dragging || marquee_active);
291 }
292}
293
294fn update_button_label<F: QueryFilter>(
295 entity: Entity,
296 button_children: &Query<&Children>,
297 button_texts: &mut Query<&mut Text, F>,
298 label: &str,
299) {
300 let Ok(children) = button_children.get(entity) else {
301 return;
302 };
303 for child in children.iter() {
304 if let Ok(mut text) = button_texts.get_mut(child) {
305 *text = Text::new(label);
306 break;
307 }
308 }
309}
310
311fn button_background(active: bool, interaction: Interaction) -> BackgroundColor {
312 match interaction {
313 Interaction::Pressed => BUTTON_ACTIVE_BACKGROUND.into(),
314 Interaction::Hovered if active => BUTTON_ACTIVE_BACKGROUND.into(),
315 Interaction::Hovered => BUTTON_HOVER_BACKGROUND.into(),
316 Interaction::None if active => BUTTON_ACTIVE_BACKGROUND.into(),
317 Interaction::None => BUTTON_BACKGROUND.into(),
318 }
319}