use gtk::{glib, prelude::*, Box, Entry, Label, ListBox}; use gtk::{Application, ApplicationWindow, Button}; use log::error; use serde::{Deserialize, Serialize}; use tokio::runtime::Handle; use tokio_tungstenite::tungstenite::Message; use crate::config::{load_config, save_config}; // use crate::{joystick_loop, JoystickThreadUpdate}; use crate::coordinator::{start_coordinator, ApplicationEvent, MoveEvent}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AppState { pub ip: String, pub port: u32, } impl Default for AppState { fn default() -> Self { AppState { ip: "10.0.0.30".to_string(), port: 8765, } } } pub enum GuiUpdate { SocketState(bool), MoveEvent(MoveEvent), } pub fn build_ui(app: &Application, runtime: Handle) { let initial_settings = load_config(); let main_box = ListBox::new(); // Main Event Channel let (to_mec, mec) = async_channel::unbounded::(); let (to_gui, gui_recv) = async_channel::bounded::(10); runtime.spawn(start_coordinator( mec, to_mec.clone(), to_gui, runtime.clone(), )); // let conn_status_label = Label::new(Some(&"No Connection".to_string())); let conn_status_label = Label::builder() .label("No Connection".to_string()) .can_focus(true) .build(); let content_box = Box::builder() .orientation(gtk::Orientation::Vertical) .spacing(10) .margin_top(12) .margin_start(24) .margin_end(24) .margin_bottom(12) .build(); let ip_entry = Entry::builder() .placeholder_text("IP Address") .text(initial_settings.ip) .can_focus(true) .build(); let port_entry = Entry::builder() .placeholder_text("Port") .text(initial_settings.port.to_string()) .can_focus(true) .build(); let button = Button::builder().margin_top(12).build(); let button2 = Button::builder().margin_top(12).build(); content_box.append(&ip_entry); content_box.append(&port_entry); content_box.append(&button); let axis_label = Label::builder() .label("X: 0 Y: )") .justify(gtk::Justification::Center) .css_classes(vec!["JoystickCurrent"]) .build(); main_box.append(&conn_status_label); main_box.append(&content_box); main_box.append(&axis_label); main_box.append(&button2); // Connect to "clicked" signal of `button` button.connect_clicked(glib::clone!(@weak ip_entry, @weak port_entry, @strong to_mec => move |_button| { let ip_text = ip_entry.text(); let port_text = port_entry.text(); if ip_text.len() == 0 { return; } if let Ok(val) = port_text.parse::() { if let Err(error) = save_config(&AppState { ip: ip_text.to_string(), port: val }) { error!("{error}"); }; match to_mec.try_send(ApplicationEvent::StartSocket( format!("ws://{}:{}", ip_text, val), )) { Ok(_) => {}, Err(async_channel::TrySendError::Closed(_)) => panic!("Coordinator MEC is closed. Unrecoverable error."), Err(e) => error!("There was an error sending to the MEC: {}", e), } } })); button2.connect_clicked(glib::clone!(@strong to_mec => move |_button| { if let Err(e) = to_mec.try_send(ApplicationEvent::SocketMessage(Message::text("U45:L10"), 1)) { panic!("There was an error in connect clicked: {e}"); } })); glib::spawn_future_local( glib::clone!(@weak axis_label, @weak button, @weak conn_status_label, @weak ip_entry, @weak port_entry, @strong gui_recv => async move { while let Ok(d) = gui_recv.recv().await { match d { GuiUpdate::MoveEvent(msg) => { axis_label.set_text( format!("X: {:>4} Y: {:>4}", msg.x, msg.y).as_str() ); } GuiUpdate::SocketState(v) => { let label = { if v { ip_entry.set_sensitive(false); port_entry.set_sensitive(false); "Currently Connected" } else { ip_entry.set_sensitive(true); port_entry.set_sensitive(true); "Currently Disconnected" } }; button.set_label(label); conn_status_label.set_label(label); if v { conn_status_label.set_css_classes(&["YesConnection"]); button.set_css_classes(&["YesConnection"]); } else { conn_status_label.set_css_classes(&["NoConnection"]); button.set_css_classes(&["NoConnection"]); } } } } }), ); // Create a window let window = ApplicationWindow::builder() .application(app) .title("VCC Camera Controller") .child(&main_box) .build(); window.connect_close_request(move |_| glib::Propagation::Proceed); // Present window window.present(); }