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},
9 },
10 feathers::{
11 constants::icons,
12 containers::{
13 flex_spacer, group, group_body, group_header, pane, pane_body, pane_header, subpane,
14 subpane_body, subpane_header,
15 },
16 controls::{ButtonVariant, FeathersButton, FeathersToolButton},
17 display::{icon, label},
18 theme::ThemedText,
19 },
20 picking::events::{PointerEnter, PointerLeave},
21 scene::{CommandsSceneExt, Scene, SceneList, bsn, on},
22 state::{
23 state::{ComputedStates, NextState, State},
24 state_scoped::DespawnOnExit,
25 },
26 ui::{
27 AlignItems, Display, FlexDirection, JustifyContent, Node, PositionType, percent, px,
28 widget::Text,
29 },
30 ui_widgets::Activate,
31};
32
33use crate::{
34 sim::track::node::{Connection, TrackNode},
35 ui::{
36 EditingWindow, IsHoveringUi, UiState,
37 target::Target,
38 track::{highlight_segment, remove_highlight_segment},
39 },
40};
41
42#[derive(Clone, Component, Default)]
43pub struct NodeWindow;
44
45fn connection_group(i: usize, c: &Connection) -> impl Scene {
46 bsn! {
47 @group()
48 Children [
49 @group_header() Children [ Text(format!("Connection #{}", i)) ThemedText ]
50 --
51 @group_body() Children [ Text(format!("Segment: {}", c.segment)) ThemedText ]
52 ]
53 Target({c.segment})
54 on(highlight_segment)
55 on(remove_highlight_segment)
56 }
57}
58
59fn no_active_connection() -> impl SceneList {
60 bsn! {
61 @label("No active connection")
62 }
63}
64
65fn segment_detail(c: &Connection) -> impl Scene {
66 bsn! {
67 @label(format!("Segment: {}", c.segment))
68 Target({c.segment})
69 on(highlight_segment)
70 on(remove_highlight_segment)
71 }
72}
73
74fn active_connection_details(
75 node_entity: Entity,
76 ac: (&Connection, &Connection),
77) -> impl SceneList {
78 bsn! {
79 @group() Children [
80 @group_header() Children [ Text("Between:") ThemedText ]
81 --
82 @group_body() Children [
83 @segment_detail(ac.0)
84 --
85 @segment_detail(ac.1)
86 ]
87 ]
88 --
89 @group() Children [
90 @group_header() Children [ Text("Switch connection:") ThemedText ]
91 --
92 @group_body() Children [
93 @FeathersButton {
94 @caption: bsn!{ Text("Next connection") ThemedText }
95 }
96 Target(node_entity)
97 on(|e: On<Activate>, q_targets: Query<&Target>, mut q_nodes: Query<&mut TrackNode>| {
98 q_nodes.get_mut(q_targets.get(e.event_target()).unwrap().0).unwrap().connections.activate_next();
99 })
100 ]
101 ]
102 }
103}
104
105pub fn node_window(node_entity: Entity, node: &TrackNode) -> impl Scene {
106 let connections_groups: Vec<_> = node
107 .connections
108 .iter()
109 .enumerate()
110 .map(|(i, c)| connection_group(i, c))
111 .collect();
112
113 let active_conn: Box<dyn SceneList> = match node.connections.active() {
114 Some(active) => Box::new(active_connection_details(node_entity, active)),
115 None => Box::new(no_active_connection()),
116 };
117
118 bsn! {
119 NodeWindow
120 EditingWindow
121 Node {
122 position_type: PositionType::Absolute,
123 display: Display::Flex,
124 flex_direction: FlexDirection::Column,
125 align_items: AlignItems::Stretch,
126 justify_content: JustifyContent::Start,
127 padding: px(8),
128 row_gap: px(8),
129 width: percent(30),
130 min_width: px(200),
131 }
132 DespawnOnExit::<UiState>(UiState::NodeWindow { target: node_entity })
133 Children [
134 @pane() Children [
135 @pane_header() Children [
136 Text(format!("Track node ({})", node_entity)) ThemedText
137 --
138 @flex_spacer()
139 --
140 @FeathersToolButton {
141 @variant: ButtonVariant::Plain,
142 }
143 Children [
144 @icon(icons::X)
145 ]
146 on(|_: On<Activate>, mut next_state: ResMut<NextState<UiState>>| {
147 next_state.set(UiState::Default);
148 })
149 ]
150 --
151 @pane_body() Children [
152 @subpane() Children [
153 @subpane_header() Children [ Text("Connections") ThemedText ]
154 --
155 @subpane_body() Children [{connections_groups}]
156 ]
157 --
158 @subpane() Children [
159 @subpane_header() Children [ Text("Active connection") ThemedText ]
160 --
161 @subpane_body() Children [{active_conn}]
162 ]
163 ]
164 ]
165 ]
166 on(|_: On<PointerEnter>, mut next_state: ResMut<NextState<IsHoveringUi>>| {
167 next_state.set(IsHoveringUi(true));
168 })
169 on(|_: On<PointerLeave>, mut next_state: ResMut<NextState<IsHoveringUi>>| {
170 next_state.set(IsHoveringUi(false));
171 })
172 }
173}
174
175#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
176pub struct IsNodeWindow;
177
178impl ComputedStates for IsNodeWindow {
179 type SourceStates = UiState;
180
181 fn compute(sources: Self::SourceStates) -> Option<Self> {
182 match sources {
183 UiState::NodeWindow { .. } => Some(IsNodeWindow),
184 _ => None,
185 }
186 }
187}
188
189pub fn open_node_window(
190 mut cmd: Commands,
191 q_nodes: Query<&TrackNode>,
192 curr_state: Res<State<UiState>>,
193) {
194 let UiState::NodeWindow { target } = curr_state.get() else {
195 panic!("`open_node_window` ran from the wrong state!");
196 };
197
198 let Ok(target_node) = q_nodes.get(*target) else {
199 return;
200 };
201
202 cmd.spawn_scene(node_window(*target, target_node));
203}