Skip to main content

traaains/ui/track/
node.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},
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}