Skip to main content

traaains/ui/
mod.rs

1//! Module containing all of the app's UI.
2//!
3//! UI is further split between individual modules, each covering a different aspect of the simulation (tracks, blocks ...).
4//! The UI is managed via a central [UiState]. Each different window then has a separate state defined. Things like "close" buttons
5//! set the state back to [UiState::Default] with their respective windows reacting via DespawnOnExit. This means that only
6//! window "type" can be opened at a time.
7//!
8//! TODO: Reactive UI. Most of the UI is static, meaning it won't change when the world properties change.
9
10use bevy::{
11    app::{Plugin, Startup, Update},
12    ecs::{
13        component::Component,
14        entity::Entity,
15        hierarchy::Children,
16        observer::On,
17        query::With,
18        system::{Commands, Query, Res, ResMut},
19    },
20    feathers::{controls::FeathersButton, theme::ThemedText},
21    picking::events::{PointerEnter, PointerLeave},
22    scene::{CommandsSceneExt, Scene, bsn, on},
23    state::{
24        app::AppExtStates,
25        state::{NextState, OnEnter, State, SubStates},
26    },
27    ui::{Display, FlexDirection, Node, PositionType, prelude::AccessibleLabel, px, widget::Text},
28    ui_widgets::Activate,
29};
30
31use crate::{
32    AppState,
33    sim::{logical_graph::LogicalBlock, train::Train},
34    ui::{
35        logical_blocks::logical_blocks_window,
36        target::Target,
37        track::{
38            node::{IsNodeWindow, open_node_window},
39            segment::{self, IsSegmentWindow, open_segment_window},
40        },
41        train::train_window::{IsTrainsWindow, open_trains_window, update_vel_indicator},
42    },
43};
44
45pub mod logical_blocks;
46pub mod target;
47pub mod track;
48pub mod train;
49
50/// The central UI state.
51#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, SubStates)]
52#[source(AppState = AppState::Main)]
53pub enum UiState {
54    #[default]
55    Default,
56    SegmentWindow {
57        target: Entity,
58    },
59    NodeWindow {
60        target: Entity,
61    },
62    TrainWindow {
63        target: Entity,
64    },
65    BlocksWindow,
66    TrainsOverlay,
67    AddingTrain,
68}
69
70/// Whether the user is hovering over any UI.
71#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, SubStates)]
72#[source(AppState = AppState::Main)]
73pub struct IsHoveringUi(pub bool);
74
75/// Entities marked with this component should be treated as windows for editing different aspects of the simulation.
76#[derive(Component, Clone, Default)]
77pub struct EditingWindow;
78
79pub fn on_activate_logical_blocks(
80    _: On<Activate>,
81    mut cmd: Commands,
82    logical_blocks: Query<(Entity, &LogicalBlock)>,
83    curr_state: Res<State<UiState>>,
84    mut next_state: ResMut<NextState<UiState>>,
85) {
86    if *curr_state == UiState::BlocksWindow {
87        return;
88    }
89
90    next_state.set(UiState::BlocksWindow);
91    cmd.spawn_scene(logical_blocks_window(logical_blocks.iter()));
92}
93
94pub fn on_activate_trains(
95    _: On<Activate>,
96    mut cmd: Commands,
97    q_trains: Query<Entity, With<Train>>,
98    curr_state: Res<State<UiState>>,
99    mut next_state: ResMut<NextState<UiState>>,
100) {
101    if *curr_state == UiState::TrainsOverlay {
102        return;
103    };
104
105    next_state.set(UiState::TrainsOverlay);
106    cmd.spawn_scene(train::trains_overlay(q_trains.iter()));
107}
108
109pub fn toolbar() -> impl Scene {
110    bsn! {
111        Node {
112            position_type: PositionType::Absolute,
113            left: px(8),
114            bottom: px(8),
115            display: Display::Flex,
116            flex_direction: FlexDirection::Row,
117            column_gap: px(8),
118            padding: px(8)
119        }
120        Children [
121            @FeathersButton {
122                @caption: bsn! { Text("Logical blocks") ThemedText }
123            }
124            AccessibleLabel("Logical blocks")
125            on(on_activate_logical_blocks)
126            --
127            @FeathersButton {
128                @caption: bsn! { Text("Trains") ThemedText }
129            }
130            AccessibleLabel("Trains")
131            on(on_activate_trains)
132        ]
133        on(|_: On<PointerEnter>, mut next_state: ResMut<NextState<IsHoveringUi>>| {
134            next_state.set(IsHoveringUi(true));
135        })
136        on(|_: On<PointerLeave>, mut next_state: ResMut<NextState<IsHoveringUi>>| {
137            next_state.set(IsHoveringUi(false));
138        })
139    }
140}
141
142fn setup_ui(mut cmd: Commands) {
143    cmd.spawn_scene(toolbar());
144}
145
146fn on_enter_default(mut is_hovering_ui: ResMut<NextState<IsHoveringUi>>) {
147    is_hovering_ui.set(IsHoveringUi(false));
148}
149
150pub struct UiPlugin;
151
152impl Plugin for UiPlugin {
153    fn build(&self, app: &mut bevy::app::App) {
154        app.init_state::<UiState>()
155            .add_sub_state::<IsHoveringUi>()
156            .add_computed_state::<IsSegmentWindow>()
157            .add_computed_state::<IsNodeWindow>()
158            .add_computed_state::<IsTrainsWindow>()
159            .add_systems(OnEnter(UiState::Default), on_enter_default)
160            .add_systems(OnEnter(IsSegmentWindow), open_segment_window)
161            .add_systems(OnEnter(IsNodeWindow), open_node_window)
162            .add_systems(OnEnter(IsTrainsWindow), open_trains_window)
163            .add_systems(Startup, setup_ui)
164            .add_systems(Update, update_vel_indicator)
165            .add_systems(
166                Update,
167                segment::signals_list::item::update_signal_list_items,
168            );
169    }
170}