1use common::logging::LogLevel::*;
8use common::logging::{FileSink, Logger, TerminalSink};
9use common::networking::messages::UserRole;
10use common::networking::messages::{TcpClientMessage, TcpServerMessage};
11use common::networking::subscription_objects::SubscribeTopic::DMXConfiguration;
12use common::r_log;
13use eframe::egui;
14use egui_dock::{DockArea, DockState, TabViewer};
15use std::sync::mpsc::{Receiver, Sender};
16use std::sync::{mpsc, LazyLock, RwLock};
17use std::net::ToSocketAddrs;
18use crate::controller::{send_ui_event, UiEvent};
19use crate::network::udp_client;
20use crate::network::udp_client::MAX_CHANNEL;
21use crate::network::connection_state::ConnectionState::Connected;
22use network::connection_state::{ConnectionState, SessionState};
23use network::tcp_client::TcpClient;
24use panels::Tab;
25use crate::network::connection_state::SessionState::LoggedIn;
26
27mod controller;
28mod network;
29mod panels;
30
31pub static UI_EVENT_SENDER: LazyLock<RwLock<Option<Sender<UiEvent>>>> =
33 LazyLock::new(|| RwLock::new(None));
34
35fn main() -> eframe::Result<()> {
40 let log_path = std::env::temp_dir().join("rektal_gui.log");
41 Logger::global().add_sink(Box::new(FileSink::new(log_path.to_str().unwrap_or("/tmp/rektal_gui.log"))));
42 Logger::global().add_sink(Box::new(TerminalSink { cli_prompt: None }));
43
44 let icon = {
45 let image = img_crate::load_from_memory(include_bytes!("../assets/rektal-logo-without-title-32px.png"))
46 .expect("Icon konnte nicht geladen werden")
47 .into_rgba8();
48 let width = image.width();
49 let height = image.height();
50 egui::viewport::IconData {
51 rgba: image.into_raw(),
52 width,
53 height,
54 }
55 };
56
57 let options = eframe::NativeOptions {
58 viewport: egui::ViewportBuilder::default()
59 .with_inner_size([1024.0, 768.0])
60 .with_title("R.E.K.T.A.L.")
61 .with_app_id("rektal")
62 .with_icon(std::sync::Arc::new(icon)),
63 ..Default::default()
64 };
65 r_log!(Info, "eframe initialized");
66
67 eframe::run_native(
68 "Docking App",
69 options,
70 Box::new(|cc| Ok(Box::new(MyApp::new(cc.egui_ctx.clone())))),
71 )
72}
73
74pub struct MyApp {
79 tree: DockState<Tab>,
81 show_session_settings: bool,
83 show_connection_settings: bool,
85 server_address: String,
87 username: String,
89 next_tab_id: u32,
91 dmx_receiver: Receiver<(u8, [u8; MAX_CHANNEL])>,
93 tcp_listen_receiver: Option<Receiver<TcpServerMessage>>,
95 ui_event_sender: Sender<UiEvent>,
97 ui_event_receiver: Receiver<UiEvent>,
99 tcp_write_sender: Option<Sender<TcpClientMessage>>,
101 connection_state: ConnectionState,
103 role: UserRole,
105 password: String,
107 draft_username: String,
109 draft_role: UserRole,
111}
112
113impl MyApp {
114 fn new(ctx: egui::Context) -> Self {
119 let (dmx_sender, dmx_receiver) = mpsc::channel();
120
121 let (ui_event_sender, ui_event_receiver) = mpsc::channel();
122
123 *UI_EVENT_SENDER.write().unwrap() = Some(ui_event_sender.clone());
124
125 if let Err(e) = udp_client::start_udp_listener(None, dmx_sender, ctx) {
126 r_log!(Error, "Failed to start UDP listener: {}", e);
127 }
128
129 let initial_terminal_panel = panels::terminal::TerminalPanel::new(0, false);
130
131 let mut app = Self {
132 tree: DockState::new(vec![Tab::Terminal(initial_terminal_panel)]),
133 show_session_settings: false,
134 show_connection_settings: false,
135 server_address: "127.0.0.1".to_string(),
136 username: "Default User".to_string(),
137 next_tab_id: 1,
138 dmx_receiver,
139 tcp_listen_receiver: None,
140 ui_event_sender,
141 ui_event_receiver,
142 tcp_write_sender: None,
143 connection_state: ConnectionState::Disconnected,
144 role: UserRole::Programmer,
145 password: String::new(),
146 draft_username: String::new(),
147 draft_role: UserRole::Programmer,
148 };
149
150 if let Some(target) = parse_args() {
151 app.server_address = target;
152 app.connect_to_server();
153 }
154
155 app
156 }
157
158 fn connect_to_server(&mut self) {
160 let (tcp_write_sender, tcp_write_receiver) = mpsc::channel();
161 let (tcp_listen_sender, tcp_listen_receiver) = mpsc::channel();
162 self.tcp_write_sender = Some(tcp_write_sender);
163 self.tcp_listen_receiver = Some(tcp_listen_receiver);
164
165 let server_address_clone = self.server_address.clone();
166 std::thread::spawn(move || {
167 let target_raw = if server_address_clone.contains(':') {
168 server_address_clone
169 } else {
170 format!("{}:6767", server_address_clone)
171 };
172
173 if target_raw.to_socket_addrs().is_ok() {
174 let mut tcp_client = TcpClient::new(
175 target_raw,
176 tcp_write_receiver,
177 tcp_listen_sender,
178 );
179 tcp_client.start_tcp_client();
180 } else {
181 r_log!(Error, "Failed to resolve server address: {}", target_raw);
182 send_ui_event(UiEvent::SetConnectionState {
183 state: ConnectionState::Error,
184 });
185 }
186 });
187 }
188
189 fn draw_top_bar(&mut self, ctx: &egui::Context) {
194 egui::TopBottomPanel::top("top_bar").show(ctx, |ui| {
195 egui::menu::bar(ui, |ui| {
196 ui.menu_button("Connection", |ui| {
197 if ui.button("Connection Settings").clicked() {
198 self.show_connection_settings = true;
199 ui.close_menu();
200 }
201 if ui
202 .add_enabled(
203 matches!(self.connection_state, Connected { .. }),
204 egui::Button::new("Session Settings"),
205 )
206 .clicked()
207 {
208 self.show_session_settings = true;
209 self.draft_username = self.username.clone();
210 self.draft_role = self.role.clone();
211 self.password.clear(); ui.close_menu();
213 }
214 if ui
215 .add_enabled(
216 matches!(
217 self.connection_state,
218 Connected {
219 session_state: SessionState::LoggedIn
220 }
221 ),
222 egui::Button::new("Logout"),
223 )
224 .clicked()
225 {
226 let event = UiEvent::LogoutRequest;
227 if let Err(e) = self.ui_event_sender.send(event) {
228 r_log!(Error, "Failed to send UiEvent: {}", e);
229 }
230 }
231 if ui
232 .add_enabled(
233 !matches!(self.connection_state, ConnectionState::Disconnected),
234 egui::Button::new("Disconnect"),
235 )
236 .clicked()
237 {
238 let event = UiEvent::DisconnectRequest;
239 if let Err(e) = self.ui_event_sender.send(event) {
240 r_log!(Error, "Failed to send UiEvent: {}", e);
241 }
242 }
243 });
244
245 ui.menu_button("Window", |ui| {
246
247 if ui.button("Terminal").clicked() {
248 let new_tab_id = self.next_tab_id;
249 self.next_tab_id += 1;
250
251 let new_terminal_panel = panels::terminal::TerminalPanel::new(
252 new_tab_id,
253 self.connection_state == Connected{session_state: LoggedIn});
254
255 self.tree
256 .main_surface_mut()
257 .push_to_focused_leaf(Tab::Terminal(new_terminal_panel));
258 ui.close_menu();
259 }
260
261 if ui.button("Universe").clicked() {
262 let event = UiEvent::SubscribeRequest {topic: DMXConfiguration};
263 if let Err(e) = self.ui_event_sender.send(event) {
264 r_log!(Error, "Failed to send UiEvent: {}", e);
265 }
266
267 let new_tab_id = self.next_tab_id;
268 self.next_tab_id += 1;
269
270 let new_universe_panel = panels::universe::UniversePanel::new(new_tab_id);
271 self.tree
272 .main_surface_mut()
273 .push_to_focused_leaf(Tab::Universe(new_universe_panel));
274 ui.close_menu();
275 }
276 });
277 });
278 });
279 }
280
281 fn draw_central_panel(&mut self, ctx: &egui::Context) {
286 egui::CentralPanel::default().show(ctx, |ui| {
287 let mut tab_viewer = MyTabViewer {};
288 DockArea::new(&mut self.tree)
289 .style(egui_dock::Style::from_egui(ui.style().as_ref()))
290 .show_inside(ui, &mut tab_viewer);
291 });
292 }
293
294 fn draw_connection_settings(&mut self, ctx: &egui::Context) {
299 let mut is_connection_open = self.show_connection_settings;
300 let mut request_conn_close = false;
301
302 if is_connection_open {
303 egui::Window::new("Connection Settings")
304 .open(&mut is_connection_open)
305 .collapsible(false)
306 .resizable([false, false])
307 .pivot(egui::Align2::CENTER_CENTER)
308 .show(ctx, |ui| {
309 egui::Grid::new("connection_seetings_grid")
310 .num_columns(2)
311 .show(ui, |ui| {
312 ui.label("Server adress ");
313 if ui.text_edit_singleline(&mut self.server_address).changed() {}
314 });
315 ui.add_space(10.0);
316
317 ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
318 if ui.button("Connect").clicked() {
319 self.connect_to_server();
320 request_conn_close = true;
321 }
322 if ui.button("Close").clicked() {
323 request_conn_close = true;
324 }
325 });
326 });
327 }
328
329 if request_conn_close {
330 is_connection_open = false;
331 }
332 self.show_connection_settings = is_connection_open;
333 }
334
335 fn draw_session_settings(&mut self, ctx: &egui::Context) {
340 let mut is_session_open = self.show_session_settings;
341 let mut request_close = false;
342 if is_session_open {
343 egui::Window::new("Session Settings")
344 .open(&mut is_session_open)
345 .collapsible(false)
346 .resizable([false, false])
347 .pivot(egui::Align2::CENTER_CENTER)
348 .show(ctx, |ui| {
349 egui::Grid::new("session_seetings_grid")
350 .num_columns(2)
351 .show(ui, |ui| {
352 ui.label("Username ");
353 ui.text_edit_singleline(&mut self.draft_username);
354 ui.end_row();
355
356 ui.label("Role");
357 egui::ComboBox::from_id_source("role_combo")
358 .selected_text(match self.draft_role {
359 UserRole::Programmer => "Programmer",
360 UserRole::BlindProgrammer => "Programmer Blind",
361 UserRole::Showrunner => "Showrunner",
362 })
363 .show_ui(ui, |ui| {
364 ui.selectable_value(
365 &mut self.draft_role,
366 UserRole::Programmer,
367 "Programmer",
368 );
369 ui.selectable_value(
370 &mut self.draft_role,
371 UserRole::BlindProgrammer,
372 "Programmer Blind",
373 );
374 ui.selectable_value(
375 &mut self.draft_role,
376 UserRole::Showrunner,
377 "Showrunner",
378 );
379 });
380 ui.end_row();
381
382 ui.label("Password");
383 let password_edit =
384 egui::TextEdit::singleline(&mut self.password).password(true);
385 ui.add(password_edit);
386 ui.end_row();
387 });
388 ui.add_space(10.0);
389
390 if let Connected {
391 session_state: SessionState::LoginFailed(ref reason),
392 } = self.connection_state
393 {
394 ui.label(
395 egui::RichText::new(format!("Login fehlgeschlagen: {}", reason))
396 .color(egui::Color32::RED),
397 );
398 ui.add_space(10.0);
399 }
400
401 ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
402 if ui.button("Login").clicked() {
403 self.username = self.draft_username.clone();
404 self.role = self.draft_role.clone();
405
406 let event = UiEvent::LoginRequest {
407 user_name: self.username.clone(),
408 password: self.password.clone(),
409 user_role: self.role.clone(),
410 };
411 if let Err(e) = self.ui_event_sender.send(event) {
412 r_log!(Error, "Failed to send UiEvent: {}", e);
413 } else {
414 request_close = true;
415 }
416 self.password.clear();
417 }
418 if ui.button("Close").clicked() {
419 request_close = true;
420 }
421 });
422 });
423 }
424 if request_close {
425 is_session_open = false;
426 }
427 self.show_session_settings = is_session_open;
428 }
429
430 fn draw_bottom_bar(&mut self, ctx: &egui::Context) {
435 egui::TopBottomPanel::bottom("bottom_bar").show(ctx, |ui| {
436 ui.horizontal(|ui| {
437 let status_color = match &self.connection_state {
438 ConnectionState::Disconnected | ConnectionState::Error => egui::Color32::RED,
439 ConnectionState::ConnectionPending => egui::Color32::YELLOW,
440 Connected { session_state } => match session_state {
441 SessionState::LoginFailed(_) => egui::Color32::YELLOW,
442 SessionState::LoggedIn => egui::Color32::GREEN,
443 SessionState::LoginPending | SessionState::LoggedOut => {
444 egui::Color32::YELLOW
445 }
446 },
447 };
448 let (rect, _) = ui.allocate_exact_size(egui::vec2(8.0, 8.0), egui::Sense::hover());
449 ui.painter().circle_filled(rect.center(), 4.0, status_color);
450 if let Connected {
451 session_state: SessionState::LoggedIn,
452 } = self.connection_state
453 {
454 ui.label(format!("{} | {}", self.username, self.role.to_string()));
455 } else {
456 ui.label("Logged out");
457 }
458
459 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
460 ui.label(format!(
461 "R.E.K.T.A.L. Version: {}",
462 env!("CARGO_PKG_VERSION")
463 ));
464 });
465 });
466 });
467 }
468}
469
470impl eframe::App for MyApp {
471 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
476 controller::handle_dmx_data(&self.dmx_receiver, &mut self.tree);
477 controller::handle_incoming_network_data(
478 &mut self.tcp_listen_receiver,
479 &mut self.tree,
480 );
481 controller::handle_events(
482 &self.ui_event_receiver,
483 &mut self.tcp_write_sender,
484 &mut self.connection_state,
485 &mut self.tree,
486 );
487
488 self.draw_top_bar(ctx);
489 self.draw_bottom_bar(ctx);
490 self.draw_central_panel(ctx);
491 self.draw_connection_settings(ctx);
492 self.draw_session_settings(ctx);
493 }
494
495 fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
499 r_log!(Info, "Shutting down..");
500
501 if let Some(sender) = self.tcp_write_sender.take() {
502 let _ = sender.send(TcpClientMessage::Logout);
503 }
504
505 send_ui_event(UiEvent::DisconnectRequest);
506 }
507}
508
509fn parse_args() -> Option<String> {
514 let args: Vec<String> = std::env::args().collect();
515 let mut iter = args.iter();
516
517 while let Some(arg) = iter.next() {
518 if arg == "-c" || arg == "--connect" || arg == "-conn" {
519 if let Some(target) = iter.next() {
520 return Some(target.clone());
521 }
522 }
523 }
524 None
525}
526
527struct MyTabViewer;
529
530impl TabViewer for MyTabViewer {
531 type Tab = Tab;
532
533 fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText {
535 tab.title().into()
536 }
537
538 fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) {
540 tab.ui(ui);
541 }
542
543 fn id(&mut self, tab: &mut Self::Tab) -> egui::Id {
545 egui::Id::new(tab.unique_id())
546 }
547}