traaains/ui/track/segment/
mod.rs1use 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 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
176pub 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 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}