ak_vis/ui/playback/
systems.rs

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}