Skip to main content

node_gui/
ui.rs

1// #t(file: rust_hardcoded_url) user-facing product URLs opened in browser
2
3//! Slint window construction, callback wiring, and event-loop polling.
4
5use std::{cell::RefCell, rc::Rc, time::Duration};
6
7use copypasta::{ClipboardContext, ClipboardProvider};
8use slint::ComponentHandle;
9use wowlab_node::{init_ui_logging, log_dir};
10
11use crate::{
12    AppWindow,
13    app::{App, AppInitError},
14    tray, update,
15};
16
17const POLL_INTERVAL: Duration = Duration::from_millis(100);
18const CLIPBOARD_FEEDBACK_DURATION: Duration = Duration::from_secs(2);
19
20#[derive(Debug, thiserror::Error)]
21pub(crate) enum UiError {
22    #[error(transparent)]
23    Application(#[from] AppInitError),
24    #[error("application window failed: {0}")]
25    Window(#[from] slint::PlatformError),
26}
27
28pub(crate) fn run(no_update: bool) -> Result<(), UiError> {
29    let (_logging_guard, log_rx) = init_ui_logging();
30
31    let window = AppWindow::new()?;
32    let app = Rc::new(RefCell::new(App::new(log_rx)?));
33
34    initialize_window(&window, &app.borrow());
35    wire_node_callbacks(&window, &app);
36    wire_utility_callbacks(&window, &app);
37    wire_settings_callbacks(&window, &app);
38
39    if update::should_check_on_startup(no_update) {
40        update::check_in_background(&window.as_weak());
41    }
42
43    let tray_state = tray::create_tray();
44    let timer = start_poll_timer(&window, &app, tray_state);
45
46    window.run()?;
47    drop(timer);
48
49    Ok(())
50}
51
52fn initialize_window(window: &AppWindow, app: &App) {
53    window.set_version_text(format!("Version {}", crate::VERSION).into());
54    window.set_cpu_cores(app.cpu_cores().to_string().into());
55    window.set_node_name(app.node_name().into());
56    window.set_public_key(app.public_key().into());
57    window.set_app_state(app.state_name().into());
58    window.set_connection_status(app.connection_status_name().into());
59}
60
61fn wire_node_callbacks(window: &AppWindow, app: &Rc<RefCell<App>>) {
62    let window_weak = window.as_weak();
63
64    window.on_token_changed(move |text| {
65        let (error, valid) = crate::app::validate_token(&text);
66
67        if let Some(window) = window_weak.upgrade() {
68            window.set_token_error(error.into());
69            window.set_token_valid(valid);
70        }
71    });
72
73    let app_ref = Rc::clone(app);
74
75    window.on_register(move |token| {
76        app_ref.borrow_mut().register(token.to_string());
77    });
78
79    let app_ref = Rc::clone(app);
80    let window_weak = window.as_weak();
81
82    window.on_unlink(move || {
83        if let Some(window) = window_weak.upgrade() {
84            window.set_unlink_busy(true);
85            window.set_unlink_error("".into());
86        }
87
88        app_ref.borrow_mut().request_unlink();
89    });
90
91    let app_ref = Rc::clone(app);
92    let window_weak = window.as_weak();
93
94    window.on_toggle_node(move || {
95        if let Some(window) = window_weak.upgrade() {
96            let enabled = window.get_node_enabled();
97
98            if enabled {
99                app_ref.borrow_mut().disconnect();
100                window.set_node_enabled(false);
101            } else {
102                app_ref.borrow_mut().reconnect();
103                window.set_node_enabled(true);
104            }
105        }
106    });
107}
108
109fn wire_utility_callbacks(window: &AppWindow, app: &Rc<RefCell<App>>) {
110    window.on_open_portal(|| {
111        let _ = open::that("https://wowlab.gg/account/nodes?showToken");
112    });
113
114    window.on_open_logs(|| {
115        if let Some(directory) = log_dir() {
116            let _ = open::that(directory);
117        }
118    });
119
120    let app_ref = Rc::clone(app);
121    let window_weak = window.as_weak();
122
123    window.on_copy_public_key(move || {
124        let public_key = app_ref.borrow().public_key().to_string();
125
126        copy_to_clipboard_with_feedback(&public_key, &window_weak);
127    });
128
129    let window_weak = window.as_weak();
130
131    window.on_copy_log_entry(move |message| {
132        copy_to_clipboard_with_feedback(&message, &window_weak);
133    });
134
135    let app_ref = Rc::clone(app);
136    let window_weak = window.as_weak();
137
138    window.on_set_filter(move |filter| {
139        app_ref.borrow_mut().set_filter(filter);
140
141        if let Some(window) = window_weak.upgrade() {
142            app_ref.borrow().update_logs(&window);
143        }
144    });
145
146    let app_ref = Rc::clone(app);
147    let window_weak = window.as_weak();
148
149    window.on_search_changed(move |text| {
150        app_ref.borrow_mut().set_search_text(text.to_string());
151
152        if let Some(window) = window_weak.upgrade() {
153            app_ref.borrow().update_logs(&window);
154        }
155    });
156}
157
158fn wire_settings_callbacks(window: &AppWindow, app: &Rc<RefCell<App>>) {
159    let window_weak = window.as_weak();
160
161    window.on_check_for_updates(move || {
162        update::check_in_background(&window_weak);
163    });
164
165    let window_weak = window.as_weak();
166
167    window.on_toggle_minimize_to_tray(move || {
168        if let Some(window) = window_weak.upgrade() {
169            window.set_minimize_to_tray(!window.get_minimize_to_tray());
170        }
171    });
172
173    let window_weak = window.as_weak();
174    let app_ref = Rc::clone(app);
175
176    window.window().on_close_requested(move || {
177        if let Some(window) = window_weak.upgrade() {
178            if app_ref.borrow().unlink_in_progress() {
179                return slint::CloseRequestResponse::KeepWindowShown;
180            }
181
182            if window.get_minimize_to_tray() {
183                let _ = window.hide();
184
185                return slint::CloseRequestResponse::KeepWindowShown;
186            }
187        }
188
189        slint::CloseRequestResponse::HideWindow
190    });
191}
192
193fn start_poll_timer(
194    window: &AppWindow,
195    app: &Rc<RefCell<App>>,
196    tray_state: Option<tray::TrayState>,
197) -> slint::Timer {
198    let timer = slint::Timer::default();
199    let app_ref = Rc::clone(app);
200    let window_weak = window.as_weak();
201
202    timer.start(slint::TimerMode::Repeated, POLL_INTERVAL, move || {
203        if let Some(window) = window_weak.upgrade() {
204            let close_after_unlink = app_ref.borrow_mut().poll(&window);
205
206            if close_after_unlink {
207                let _ = slint::quit_event_loop();
208
209                return;
210            }
211        }
212
213        if let Some(ref tray) = tray_state {
214            match tray::poll_tray_events(tray) {
215                tray::TrayAction::Show => {
216                    if let Some(window) = window_weak.upgrade() {
217                        let _ = window.show();
218                    }
219                }
220                tray::TrayAction::Quit => {
221                    if !app_ref.borrow().unlink_in_progress() {
222                        let _ = slint::quit_event_loop();
223                    }
224                }
225                tray::TrayAction::None => {}
226            }
227        }
228    });
229
230    timer
231}
232
233fn copy_to_clipboard_with_feedback(text: &str, window: &slint::Weak<AppWindow>) {
234    if let Ok(mut clipboard) = ClipboardContext::new() {
235        if clipboard.set_contents(text.to_string()).is_ok() {
236            if let Some(window) = window.upgrade() {
237                window.set_show_copied_feedback(true);
238                let feedback_window = window.as_weak();
239
240                slint::Timer::single_shot(CLIPBOARD_FEEDBACK_DURATION, move || {
241                    if let Some(window) = feedback_window.upgrade() {
242                        window.set_show_copied_feedback(false);
243                    }
244                });
245            }
246        }
247    }
248}