Skip to main content

gui/
controller.rs

1//! # Controller Module
2//!
3//! The `controller` module acts as the central event handler and coordinator
4//! for the R.E.K.T.A.L. GUI application.
5//!
6//! It is responsible for:
7//! - Draining incoming DMX universe stream data and updating universe UI panels.
8//! - Dispatching global UI events ([`UiEvent`]) via channels.
9//! - Processing network messages ([`TcpServerMessage`]) received from the kernel server.
10//! - Handling UI action events and updating central application states ([`ConnectionState`], [`SessionState`]).
11
12use crate::network::connection_state::SessionState::{LoggedIn, LoggedOut, LoginFailed};
13use crate::network::connection_state::{ConnectionState, SessionState};
14use crate::network::udp_client::MAX_CHANNEL;
15use crate::panels::terminal::TextFragment;
16use crate::panels::Tab;
17use common::logging::LogLevel;
18use common::logging::LogLevel::*;
19use common::networking::messages::{TcpClientMessage, TcpServerMessage};
20use common::networking::subscription_objects::{SubscribeTopic, TopicPayload};
21use common::r_log;
22use eframe::egui::Color32;
23use egui_dock::DockState;
24use std::fmt;
25use std::fmt::{Display, Formatter};
26use std::sync::mpsc::{Receiver, Sender};
27
28/// Enum describing global GUI events dispatched across the application.
29///
30/// These events represent user interactions or system actions that require
31/// coordination between UI panels, the central state, or network threads.
32pub enum UiEvent {
33    /// Sends a command entered in a terminal panel to the kernel server (or processes built-in commands).
34    SendTerminalCommand {
35        /// The unique ID of the terminal tab originating the command.
36        id: u32,
37        /// The command string entered by the user.
38        command: String,
39    },
40    /// Initiates a login request to the kernel server with user credentials.
41    LoginRequest {
42        /// The plain text password entered by the user.
43        password: String,
44        /// The username for authentication.
45        user_name: String,
46        /// The requested user role (e.g., Programmer, Showrunner).
47        user_role: common::networking::messages::UserRole,
48    },
49    /// Updates the central connection and session states of the GUI application.
50    SetConnectionState {
51        /// The target connection state to apply.
52        state: ConnectionState,
53    },
54    /// Requests a user session logout from the kernel server.
55    LogoutRequest,
56    /// Forcefully disconnects the TCP network connection.
57    DisconnectRequest,
58    /// Subscribes to a data topic (e.g. DMX configuration updates) from the server.
59    SubscribeRequest {
60        /// The subscription topic requested.
61        topic: SubscribeTopic,
62    },
63}
64
65impl Display for UiEvent {
66    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
67        match self {
68            UiEvent::SendTerminalCommand { id, command } => {
69                write!(f, "SendTerminalCommand, id: {}, command: {}", id, command)
70            }
71            UiEvent::LoginRequest {
72                user_name,
73                password,
74                user_role,
75            } => write!(
76                f,
77                "LoginRequest, username: {},password: {}, role: {}",
78                user_name, password, user_role
79            ),
80            UiEvent::SetConnectionState { state } => write!(f, "SetConnectionState: {}", state),
81            UiEvent::LogoutRequest => write!(f, "LogoutRequest"),
82            UiEvent::DisconnectRequest => write!(f, "DisconnectRequest"),
83            UiEvent::SubscribeRequest { topic } => write!(f, "SubscribeRequest, topic: {}", topic),
84        }
85    }
86}
87
88/// Drains incoming DMX universe frame data from the UDP receiver channel
89/// and updates the matching Universe panels in the docking tree.
90///
91/// # Arguments
92/// * `dmx_receiver` - Receiver channel producing `(universe_id, dmx_data)` tuples.
93/// * `tree` - Mutable reference to the UI docking tree containing active tabs.
94pub(crate) fn handle_dmx_data(
95    dmx_receiver: &Receiver<(u8, [u8; MAX_CHANNEL])>,
96    tree: &mut DockState<Tab>,
97) {
98    while let Ok((universe_id, dmx_data)) = dmx_receiver.try_recv() {
99        for (_, tab) in tree.iter_all_tabs_mut() {
100            if let Tab::Universe(panel) = tab {
101                if panel.selected_universe - 1 == universe_id {
102                    panel.dmx_data.copy_from_slice(&dmx_data);
103                }
104            }
105        }
106    }
107}
108
109/// Thread-safe helper to send a [`UiEvent`] into the global [`UI_EVENT_SENDER`] channel.
110///
111/// # Arguments
112/// * `event` - The [`UiEvent`] to send.
113pub fn send_ui_event(event: UiEvent) {
114    if let Ok(guard) = crate::UI_EVENT_SENDER.read() {
115        if let Some(sender) = guard.as_ref() {
116            let _ = sender.send(event);
117        }
118    }
119}
120
121/// Processes a command entered into a terminal tab.
122///
123/// Handles built-in local terminal commands (like `"logout"`) directly or packages
124/// generic commands into a [`TcpClientMessage::ExecuteCommand`] to send to the server.
125///
126/// # Arguments
127/// * `id` - The unique ID of the terminal tab.
128/// * `command` - The command string entered by the user.
129/// * `tcp_sender` - Optional sender channel to transmit messages to the TCP network thread.
130/// * `tree` - Mutable reference to the UI docking tree to update terminal outputs.
131fn process_terminal_command(
132    id: u32,
133    command: String,
134    tcp_sender: &Option<Sender<TcpClientMessage>>,
135    tree: &mut DockState<Tab>,
136) {
137    match command.as_str() {
138        "logout" => {
139            send_ui_event(UiEvent::LogoutRequest);
140            for (_, tab) in tree.iter_all_tabs_mut() {
141                if let Tab::Terminal(panel) = tab {
142                    panel.add_fragments(vec![TextFragment {
143                        text: "[SUCCESS] Logout successful, Terminal inactive!".to_string(),
144                        color: log_level_to_color32(SuccessEvent),
145                    }]);
146                };
147            }
148        }
149        _ => {
150            let msg = TcpClientMessage::ExecuteCommand {
151                response_id: id,
152                command,
153            };
154            if let Some(tcp_sender) = tcp_sender {
155                if let Err(e) = tcp_sender.send(msg) {
156                    r_log!(Error, "Failed to send execute command: {}", e);
157                }
158            } else {
159                r_log!(
160                    Error,
161                    "Failed to send execute command: tcp sender doesn't exist"
162                );
163            }
164        }
165    }
166}
167
168/// Maps a common [`LogLevel`] enum variant to a corresponding `egui` [`Color32`] for UI rendering.
169///
170/// # Arguments
171/// * `level` - The log level to convert.
172///
173/// # Returns
174/// An `egui::Color32` representing the log level.
175pub fn log_level_to_color32(level: LogLevel) -> Color32 {
176    match level {
177        SuccessEvent => Color32::GREEN,
178        Info => Color32::BLUE,
179        Warning => Color32::YELLOW,
180        Error => Color32::RED,
181        UserError => Color32::GOLD,
182        UserSuccess => Color32::LIGHT_GREEN,
183    }
184}
185
186/// Drains incoming network messages ([`TcpServerMessage`]) from the TCP receiver channel
187/// and updates UI tabs or triggers connection state changes.
188///
189/// # Arguments
190/// * `tcp_receiver` - Mutable reference to the optional TCP network message receiver channel.
191/// * `tree` - Mutable reference to the UI docking tree.
192pub(crate) fn handle_incoming_network_data(
193    tcp_receiver: &mut Option<Receiver<TcpServerMessage>>,
194    tree: &mut DockState<Tab>,
195) {
196    if let Some(tcp_receiver) = tcp_receiver {
197        while let Ok(msg) = tcp_receiver.try_recv() {
198            match msg {
199                TcpServerMessage::CommandOutput {
200                    answer,
201                    response_id,
202                } => {
203                    for (_, tab) in tree.iter_all_tabs_mut() {
204                        if let Tab::Terminal(panel) = tab {
205                            if panel.tab_id == response_id {
206                                let (log_level, ref answer_string) = answer;
207                                panel.add_fragments(vec![TextFragment {
208                                    text: format!("[{}]: {}", log_level, answer_string),
209                                    color: log_level_to_color32(log_level),
210                                }]);
211                            }
212                        }
213                    }
214                }
215                TcpServerMessage::LoginOk { token } => {
216                    r_log!(UserSuccess, "Login Successful! Token: {}", token);
217                    send_ui_event(UiEvent::SetConnectionState {
218                        state: ConnectionState::Connected {
219                            session_state: LoggedIn,
220                        },
221                    });
222                }
223                TcpServerMessage::LoginFailed { reason } => {
224                    r_log!(UserError, "Login Failed: {}", reason);
225                    send_ui_event(UiEvent::SetConnectionState {
226                        state: ConnectionState::Connected {
227                            session_state: LoginFailed(reason),
228                        },
229                    });
230                }
231                TcpServerMessage::LogoutOk => {
232                    r_log!(UserSuccess, "Logout Successful");
233                    send_ui_event(UiEvent::SetConnectionState {
234                        state: ConnectionState::Connected {
235                            session_state: LoggedOut,
236                        },
237                    });
238                }
239                TcpServerMessage::TopicUpdate { data } => match data {
240                    TopicPayload::DMXConfiguration(dmx_config) => {
241                        for (_, tab) in tree.iter_all_tabs_mut() {
242                            if let Tab::Universe(panel) = tab {
243                                panel.device_configuration = Some(dmx_config.clone());
244                            }
245                        }
246                    }
247                },
248                TcpServerMessage::ShutdownAnnouncement => {
249                    send_ui_event(UiEvent::SetConnectionState {
250                        state: ConnectionState::Disconnected
251                    });
252                },
253                TcpServerMessage::Unauthenticated => {
254                    r_log!(UserError, "Server couldn't Unauthenticated");
255                }
256                TcpServerMessage::ReloginOk { .. } => {}
257                TcpServerMessage::ReloginFailed { .. } => {}
258                TcpServerMessage::Kicked { .. } => {}
259                TcpServerMessage::ImplicitCommandOutput { .. } => {}
260                TcpServerMessage::EditGranted { .. } => {}
261                TcpServerMessage::EditDenied { .. } => {}
262                TcpServerMessage::DropEditAck(_) => {}
263            }
264        }
265    }
266}
267
268/// Drains and processes all queued [`UiEvent`] items sent from UI interactions or network handlers.
269///
270/// Manages connection state transitions, sends outgoing TCP messages, and activates/deactivates
271/// UI panels in the docking tree accordingly.
272///
273/// # Arguments
274/// * `ui_receiver` - Receiver channel for queued [`UiEvent`]s.
275/// * `tcp_sender` - Mutable reference to the optional TCP client sender channel.
276/// * `connection_state` - Mutable reference to the application's central [`ConnectionState`].
277/// * `tree` - Mutable reference to the UI docking tree.
278pub(crate) fn handle_events(
279    ui_receiver: &Receiver<UiEvent>,
280    tcp_sender: &mut Option<Sender<TcpClientMessage>>,
281    connection_state: &mut ConnectionState,
282    tree: &mut DockState<Tab>,
283) {
284    while let Ok(event) = ui_receiver.try_recv() {
285        r_log!(Info, "UIEvent: {}" ,event);
286        match event {
287            UiEvent::SendTerminalCommand { id, command } => {
288                process_terminal_command(id, command, tcp_sender, tree);
289            }
290            UiEvent::LoginRequest {
291                password,
292                user_name,
293                user_role,
294            } => {
295                let msg = TcpClientMessage::Login {
296                    password,
297                    user_name,
298                    user_role,
299                };
300                if let Some(tcp_sender) = tcp_sender {
301                    if let Err(e) = tcp_sender.send(msg) {
302                        r_log!(Error, "Failed to send login request: {}", e);
303                    } else {
304                        *connection_state = ConnectionState::Connected {
305                            session_state: SessionState::LoginPending,
306                        };
307                    }
308                } else {
309                    r_log!(
310                        Error,
311                        "Failed to send execute command: tcp sender doesn't exist"
312                    );
313                }
314            }
315            UiEvent::SetConnectionState { state } => {
316                r_log!(Info, "New ConnectionState: {}", state);
317                for (_, tab) in tree.iter_all_tabs_mut() {
318                    if state
319                        == (ConnectionState::Connected {
320                            session_state: LoggedIn,
321                        })
322                    {
323                        tab.on_connect();
324                    }
325                    if let Tab::Terminal(panel) = tab {
326                        panel.is_active = state
327                            == (ConnectionState::Connected {
328                                session_state: LoggedIn,
329                            });
330                        if state
331                            == (ConnectionState::Connected {
332                                session_state: LoggedIn,
333                            })
334                        {
335                            panel.add_fragments(vec![TextFragment {
336                                text: "[INFO] Logins successful, Terminal ready!".to_string(),
337                                color: Color32::GREEN,
338                            }]);
339                        }
340                    }
341                }
342                *connection_state = state;
343            }
344            UiEvent::LogoutRequest => {
345                let msg = TcpClientMessage::Logout;
346                if let Some(tcp_sender) = tcp_sender {
347                    if let Err(e) = tcp_sender.send(msg) {
348                        r_log!(Error, "Failed to send logout request: {}", e);
349                    } else {
350                        for (_, tab) in tree.iter_all_tabs_mut() {
351                            if let Tab::Terminal(panel) = tab {
352                                panel.is_active = false;
353                            }
354                        }
355                    }
356                } else {
357                    r_log!(
358                        Error,
359                        "Failed to send logout request: tcp sender doesn't exist"
360                    );
361                }
362            }
363            UiEvent::DisconnectRequest => {
364                r_log!(Info, "Closing connection requested via UI");
365                *tcp_sender = None;
366                *connection_state = ConnectionState::Disconnected;
367            }
368            UiEvent::SubscribeRequest { topic } => {
369                let msg = TcpClientMessage::Subscribe {
370                    topic: topic.clone(),
371                    update_mode: common::networking::subscription_objects::UpdateMode::OnChange,
372                };
373                if let Some(tcp_sender) = tcp_sender {
374                    if let Err(e) = tcp_sender.send(msg) {
375                        r_log!(Error, "Failed to send subscribe request: {}", e);
376                    }
377                }
378            }
379        }
380    }
381}