Skip to main content

traaains/ui/track/segment/
mod.rs

1use bevy::{
2    ecs::{
3        component::Component,
4        entity::Entity,
5        event::EntityEvent,
6        hierarchy::Children,
7        observer::On,
8        system::{Commands, Query, Res, ResMut, Single},
9    },
10    feathers::{
11        constants::icons,
12        containers::{
13            flex_spacer, pane, pane_body, pane_header, subpane, subpane_body, subpane_header,
14        },
15        controls::{
16            ButtonVariant, FeathersButton, FeathersNumberInput, FeathersToolButton,
17            NumberInputValue,
18        },
19        display::{icon, label},
20        theme::ThemedText,
21    },
22    picking::events::{PointerEnter, PointerLeave},
23    scene::{CommandsSceneExt, Scene, bsn, on},
24    state::{
25        state::{ComputedStates, NextState, State},
26        state_scoped::DespawnOnExit,
27    },
28    ui::{
29        AlignItems, Display, FlexDirection, JustifyContent, Node, PositionType, percent,
30        prelude::AccessibleLabel, px, widget::Text,
31    },
32    ui_widgets::{Activate, ValueChange},
33};
34
35use crate::{
36    editor::{EditorState, signal::TargetSegment},
37    sim::{
38        signal::Signals,
39        track::{
40            pos::TrackPos,
41            segment::{TrackSegment, commands::SplitSegment, signals_manager::SignalsManager},
42        },
43    },
44    ui::{EditingWindow, IsHoveringUi, UiState, target::Target},
45};
46
47pub mod signals_list;
48
49#[derive(Component, Clone, Default)]
50pub struct SegmentWindow;
51
52#[derive(Component, Clone, Default)]
53pub struct SegmentSplitInput(f32);
54
55pub fn segment_window(
56    segment_entity: Entity,
57    segment: (&TrackSegment, &SignalsManager),
58    signals: Vec<(Entity, &Signals, &TrackPos)>,
59) -> impl Scene {
60    bsn! {
61        SegmentWindow
62        EditingWindow
63        Node {
64            position_type: PositionType::Absolute,
65            top: px(16),
66            left: px(16),
67            display: Display::Flex,
68            flex_direction: FlexDirection::Column,
69            align_items: AlignItems::Stretch,
70            justify_content: JustifyContent::Start,
71            row_gap: px(8),
72            width: percent(30),
73            min_width: px(200),
74        }
75        DespawnOnExit::<UiState>(UiState::SegmentWindow { target: segment_entity } )
76        Children [
77            @pane() Children [
78                @pane_header() Children [
79                    Text(format!("Track segment ({})", segment_entity)) ThemedText
80                    --
81                    @flex_spacer()
82                    --
83                    @FeathersToolButton {
84                        @variant: ButtonVariant::Plain,
85                    }
86                    Children [
87                        @icon(icons::X)
88                    ]
89                    on(|_: On<Activate>, mut next_state: ResMut<NextState<UiState>>| {
90                        next_state.set(UiState::Default);
91                    })
92                ]
93                --
94                @pane_body() Children [
95                    @subpane() Children [
96                        @subpane_header() Children [ Text("Segment block") ThemedText ]
97                        --
98                        @subpane_body() Children [ @label(format!("Block: {}", segment.0.block)) ]
99                    ]
100                    --
101                    @subpane() Children [
102                        @subpane_header() Children [ Text("Splitting segment") ThemedText ]
103                        --
104                        @subpane_body() Children [
105                            @label("Split index:")
106                            --
107                            @FeathersNumberInput
108                            NumberInputValue::F32(0.0)
109                            SegmentSplitInput(0.0)
110                            on(|e: On<ValueChange<f32>>, mut cmd: Commands| {
111                                cmd.entity(e.event_target()).insert(NumberInputValue::F32(e.value));
112                                cmd.entity(e.event_target()).insert(SegmentSplitInput(e.value));
113                            })
114                            --
115                            @FeathersButton {
116                                @caption: bsn! { Text("Split segment") ThemedText },
117                            }
118                            AccessibleLabel("Split segment index")
119                            Target(segment_entity)
120                            on(|e: On<Activate>, mut cmd: Commands, q_targets: Query<&Target>, idx_input: Single<&SegmentSplitInput>, mut next_state: ResMut<NextState<UiState>>| {
121                                // disallow splitting at ends
122                                if idx_input.0 == 0.0 || idx_input.0 == 1.0 {
123                                    return;
124                                }
125
126                                cmd.queue(SplitSegment::new(q_targets.get(e.event_target()).unwrap().0, idx_input.0));
127                                next_state.set(UiState::Default);
128                            })
129                        ]
130                    ]
131                    --
132                    @subpane() Children [
133                        @subpane_header() Children [ Text("Segment signals") ThemedText ]
134                        --
135                        @subpane_body() Children [
136                            @FeathersButton {
137                                @caption: bsn! { Text("Add signal") ThemedText }
138                            }
139                            Target(segment_entity)
140                            on(|e: On<Activate>, mut cmd: Commands, q_targets: Query<&Target>, mut next_ui_state: ResMut<NextState<UiState>>, mut next_hovering_state: ResMut<NextState<IsHoveringUi>>, mut next_editor_state: ResMut<NextState<EditorState>>| {
141                                cmd.insert_resource(TargetSegment(q_targets.get(e.event_target()).unwrap().0));
142                                next_editor_state.set(EditorState::Signal);
143                                next_hovering_state.set(IsHoveringUi(false));
144                                next_ui_state.set(UiState::Default);
145                            })
146                            --
147                            @signals_list::signals_list(signals)
148                        ]
149                    ]
150                ]
151            ]
152        ]
153        on(|_: On<PointerEnter>, mut next_state: ResMut<NextState<IsHoveringUi>>| {
154            next_state.set(IsHoveringUi(true));
155        })
156        on(|_: On<PointerLeave>, mut next_state: ResMut<NextState<IsHoveringUi>>| {
157            next_state.set(IsHoveringUi(false));
158        })
159    }
160}
161
162#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
163pub struct IsSegmentWindow;
164
165impl ComputedStates for IsSegmentWindow {
166    type SourceStates = UiState;
167
168    fn compute(sources: Self::SourceStates) -> Option<Self> {
169        match sources {
170            UiState::SegmentWindow { .. } => Some(IsSegmentWindow),
171            _ => None,
172        }
173    }
174}
175
176/// System that runs in the OnEnter schedule of segment ui state and opens the window
177pub fn open_segment_window(
178    mut cmd: Commands,
179    q_segments: Query<(&TrackSegment, &SignalsManager)>,
180    q_signals: Query<(Entity, &Signals, &TrackPos)>,
181    curr_state: Res<State<UiState>>,
182) {
183    let UiState::SegmentWindow { target } = curr_state.get() else {
184        panic!("`open_segment_window` ran from the wrong state!")
185    };
186
187    // Collect signals on this segment and spawn the segment popup
188    let curr_segment = q_segments.get(*target).unwrap();
189    let signals_entities = curr_segment
190        .1
191        .inner
192        .values()
193        .map(|s| q_signals.get(*s).unwrap())
194        .collect();
195
196    cmd.spawn_scene(segment_window(*target, curr_segment, signals_entities));
197}