Evan's Blog
EvanZhu0721 / 公开仓库

SBMS

镜像已同步

SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit bd87fa1e。

main/ root / src/ui
bd87fa1e· 固定源码快照
src/ui/handlers.rs32.1 KB
1use std::sync::atomic::AtomicU64;
2use std::sync::{Arc, Mutex};
3use std::thread;
4
5use crate::config::{ConfigProfiles, DisplayOverrideStore, DisplayOverrides};
6use crate::controller::{ControllerSender, DisplayOption};
7use crate::diagnostics::{self, Level};
8use crate::limits::MAX_OUTPUTS;
9
10use super::*;
11
12#[derive(Clone)]
13pub(super) struct HandlerState {
14 displays: Arc<Mutex<Vec<DisplayOption>>>,
15 groups: Arc<Mutex<Vec<GroupDraft>>>,
16 active_group: Arc<Mutex<usize>>,
17 display_overrides: Arc<Mutex<DisplayOverrides>>,
18 profile_revision: Arc<Mutex<ProfileRevision>>,
19}
20
21impl HandlerState {
22 pub(super) fn new(
23 displays: Arc<Mutex<Vec<DisplayOption>>>,
24 groups: Arc<Mutex<Vec<GroupDraft>>>,
25 active_group: Arc<Mutex<usize>>,
26 display_overrides: Arc<Mutex<DisplayOverrides>>,
27 profile_revision: Arc<Mutex<ProfileRevision>>,
28 ) -> Self {
29 Self {
30 displays,
31 groups,
32 active_group,
33 display_overrides,
34 profile_revision,
35 }
36 }
37}
38
39pub(super) fn register_lifecycle_handlers(
40 flyout: &QuickAccess,
41 sender: &ControllerSender,
42 state: &HandlerState,
43 error_revision: &Arc<AtomicU64>,
44) {
45 let flyout_weak = flyout.as_weak();
46 flyout.on_dismiss(move || {
47 if let Some(flyout) = flyout_weak.upgrade() {
48 let _ = flyout.hide();
49 }
50 });
51
52 let start_sender = sender.clone();
53 let flyout_weak = flyout.as_weak();
54 let start_state = state.clone();
55 flyout.on_start(move || {
56 if let Some(ui) = flyout_weak.upgrade() {
57 let displays = start_state
58 .displays
59 .lock()
60 .expect("display metadata poisoned");
61 let mut groups = start_state.groups.lock().expect("group drafts poisoned");
62 let active = *start_state
63 .active_group
64 .lock()
65 .expect("active group poisoned");
66 snapshot_group(&ui, &displays, &mut groups, active);
67 let validated = match validate_group_drafts(&groups) {
68 Ok(validated) => validated,
69 Err(error) => {
70 surface_persistence_error(&ui, &error);
71 return;
72 }
73 };
74 if !persist_validated_groups(&ui, &validated, active, &start_state.profile_revision) {
75 return;
76 }
77 match build_mapping_plan(&validated) {
78 Ok(plan) => {
79 for group in groups.iter_mut() {
80 group.reset_telemetry();
81 }
82 project_group_telemetry(&ui, &groups, active);
83 start_sender.start(plan);
84 }
85 Err(error) => {
86 ui.set_error_text(error.as_str().into());
87 ui.set_state_detail("Mapping plan needs attention".into());
88 }
89 }
90 }
91 });
92
93 let stop_sender = sender.clone();
94 flyout.on_stop(move || stop_sender.stop());
95 let restart_sender = sender.clone();
96 flyout.on_restart_mapping(move || restart_sender.restart_mapping());
97
98 let flyout_weak = flyout.as_weak();
99 let display_settings_error_revision = error_revision.clone();
100 flyout.on_open_display_settings(move || {
101 let Some(ui) = flyout_weak.upgrade() else {
102 return;
103 };
104 if let Err(error) = open_display_settings() {
105 diagnostics::log(
106 Level::Warn,
107 "ui",
108 "open-display-settings",
109 None,
110 error.as_str(),
111 );
112 surface_transient_error(&ui, &display_settings_error_revision, &error);
113 }
114 });
115
116 let flyout_weak = flyout.as_weak();
117 let sunshine_state = state.clone();
118 let sunshine_error_revision = error_revision.clone();
119 flyout.on_open_sunshine_panel(move || {
120 let Some(ui) = flyout_weak.upgrade() else {
121 return;
122 };
123 let groups = sunshine_state.groups.lock().expect("group drafts poisoned");
124 let active = *sunshine_state
125 .active_group
126 .lock()
127 .expect("active group poisoned");
128 let Some(port) = groups
129 .get(active)
130 .filter(|group| {
131 group.stream_only
132 && group.telemetry.ready
133 && group.telemetry.sunshine_state == SunshineState::Ready
134 })
135 .and_then(|group| group.telemetry.sunshine_port)
136 else {
137 surface_transient_error(
138 &ui,
139 &sunshine_error_revision,
140 "This Sunshine instance is not ready yet",
141 );
142 return;
143 };
144 drop(groups);
145 if let Err(error) = open_sunshine_panel(port) {
146 diagnostics::log(
147 Level::Warn,
148 "ui",
149 "open-sunshine-panel",
150 None,
151 error.as_str(),
152 );
153 surface_transient_error(&ui, &sunshine_error_revision, &error);
154 }
155 });
156}
157
158pub(super) fn register_profile_handlers(
159 flyout: &QuickAccess,
160 sender: &ControllerSender,
161 state: &HandlerState,
162 error_revision: &Arc<AtomicU64>,
163 pending_config_reload: &Arc<AtomicBool>,
164 pending_profile_restart: &Arc<AtomicBool>,
165) {
166 {
167 let ui = flyout.as_weak();
168 let state = state.clone();
169 flyout.on_create_profile(move |name| {
170 let name = name.trim().to_string();
171 let state = state.clone();
172 spawn_profile_write(
173 ui.clone(),
174 "create profile",
175 move || {
176 let store = ConfigProfileStore::default_store().map_err(|e| e.to_string())?;
177 let (snapshot, _) = store
178 .duplicate_active(&name, true)
179 .map_err(|e| e.to_string())?;
180 let profiles = store.list().map_err(|e| e.to_string())?;
181 Ok((snapshot, profiles))
182 },
183 move |_ui, snapshot| {
184 *state
185 .profile_revision
186 .lock()
187 .expect("config profile revision poisoned") = snapshot.profile;
188 },
189 );
190 });
191 }
192
193 {
194 let ui = flyout.as_weak();
195 let state = state.clone();
196 flyout.on_rename_profile(move |old_name, new_name| {
197 let old_name = old_name.to_string();
198 let new_name = new_name.trim().to_string();
199 let state = state.clone();
200 spawn_profile_write(
201 ui.clone(),
202 "rename profile",
203 move || {
204 let store = ConfigProfileStore::default_store().map_err(|e| e.to_string())?;
205 let (snapshot, active) = store
206 .rename(&old_name, &new_name)
207 .map_err(|e| e.to_string())?;
208 let profiles = store.list().map_err(|e| e.to_string())?;
209 Ok(((snapshot, active), profiles))
210 },
211 move |_ui, (snapshot, active)| {
212 if active {
213 *state
214 .profile_revision
215 .lock()
216 .expect("config profile revision poisoned") = snapshot.profile;
217 }
218 },
219 );
220 });
221 }
222
223 {
224 let ui = flyout.as_weak();
225 let sender = sender.clone();
226 let state = state.clone();
227 let error_revision = error_revision.clone();
228 let pending_config_reload = pending_config_reload.clone();
229 let pending_profile_restart = pending_profile_restart.clone();
230 flyout.on_delete_profile(move |name| {
231 let name = name.to_string();
232 let sender = sender.clone();
233 let state = state.clone();
234 let error_revision = error_revision.clone();
235 let pending_config_reload = pending_config_reload.clone();
236 let pending_profile_restart = pending_profile_restart.clone();
237 spawn_profile_write(
238 ui.clone(),
239 "delete profile",
240 move || {
241 let store = ConfigProfileStore::default_store().map_err(|e| e.to_string())?;
242 let outcome = store
243 .delete_with_fallback(&name)
244 .map_err(|e| e.to_string())?;
245 let profiles = store.list().map_err(|e| e.to_string())?;
246 Ok((outcome, profiles))
247 },
248 move |ui, outcome| {
249 if outcome.active_changed {
250 reload_or_restart_profile(
251 ui,
252 &sender,
253 &state,
254 &error_revision,
255 &pending_config_reload,
256 &pending_profile_restart,
257 );
258 }
259 },
260 );
261 });
262 }
263
264 {
265 let ui = flyout.as_weak();
266 let sender = sender.clone();
267 let state = state.clone();
268 let error_revision = error_revision.clone();
269 let pending_config_reload = pending_config_reload.clone();
270 let pending_profile_restart = pending_profile_restart.clone();
271 flyout.on_activate_profile(move |name| {
272 let name = name.to_string();
273 let sender = sender.clone();
274 let state = state.clone();
275 let error_revision = error_revision.clone();
276 let pending_config_reload = pending_config_reload.clone();
277 let pending_profile_restart = pending_profile_restart.clone();
278 spawn_profile_write(
279 ui.clone(),
280 "switch profile",
281 move || {
282 let store = ConfigProfileStore::default_store().map_err(|e| e.to_string())?;
283 let snapshot = store.activate(&name).map_err(|e| e.to_string())?;
284 let profiles = store.list().map_err(|e| e.to_string())?;
285 Ok((snapshot, profiles))
286 },
287 move |ui, _snapshot| {
288 reload_or_restart_profile(
289 ui,
290 &sender,
291 &state,
292 &error_revision,
293 &pending_config_reload,
294 &pending_profile_restart,
295 );
296 },
297 );
298 });
299 }
300}
301
302fn spawn_profile_write<T, Operation, Completion>(
303 ui: slint::Weak<QuickAccess>,
304 action: &'static str,
305 operation: Operation,
306 completion: Completion,
307) where
308 T: Send + 'static,
309 Operation: FnOnce() -> Result<(T, ConfigProfiles), String> + Send + 'static,
310 Completion: FnOnce(&QuickAccess, T) + Send + 'static,
311{
312 let Some(strong_ui) = ui.upgrade() else {
313 return;
314 };
315 if strong_ui.get_profile_saving() {
316 return;
317 }
318 strong_ui.set_error_text("".into());
319 let generation = begin_profile_save(&strong_ui);
320 drop(strong_ui);
321
322 thread::spawn(move || {
323 let result = operation();
324 let callback_ui = ui.clone();
325 if let Err(error) = slint::invoke_from_event_loop(move || {
326 let Some(strong_ui) = callback_ui.upgrade() else {
327 return;
328 };
329 match result {
330 Ok((value, profiles)) => {
331 project_profiles(&strong_ui, &profiles);
332 completion(&strong_ui, value);
333 finish_profile_save(callback_ui.clone(), generation, true);
334 }
335 Err(error) => {
336 diagnostics::log(Level::Warn, "config", action, None, &error);
337 strong_ui.set_error_text(format!("Couldn’t {action}: {error}").into());
338 finish_profile_save(callback_ui.clone(), generation, false);
339 }
340 }
341 }) {
342 diagnostics::log(
343 Level::Warn,
344 "config",
345 "profile-ui-dispatch",
346 None,
347 error.to_string(),
348 );
349 }
350 });
351}
352
353fn reload_or_restart_profile(
354 ui: &QuickAccess,
355 sender: &ControllerSender,
356 state: &HandlerState,
357 error_revision: &Arc<AtomicU64>,
358 pending_config_reload: &Arc<AtomicBool>,
359 pending_profile_restart: &Arc<AtomicBool>,
360) {
361 if ui.get_running() || ui.get_busy() {
362 pending_config_reload.store(true, Ordering::Release);
363 pending_profile_restart.store(true, Ordering::Release);
364 sender.stop();
365 return;
366 }
367 apply_config_reload(
368 ui,
369 &state.displays,
370 &state.groups,
371 &state.active_group,
372 &state.profile_revision,
373 error_revision,
374 );
375}
376
377pub(super) fn register_geometry_handlers(flyout: &QuickAccess, state: &HandlerState) {
378 let flyout_weak = flyout.as_weak();
379 let callback_state = state.clone();
380 flyout.on_display_selected(move || {
381 if let Some(ui) = flyout_weak.upgrade() {
382 let displays = callback_state
383 .displays
384 .lock()
385 .expect("display metadata poisoned");
386 let selected_id = selected_display_id(&ui, ui.get_selected_display());
387 ui.set_stream_only(selected_id.as_deref() == Some(STREAM_ONLY_ID));
388 let previous_reference = selected_reference_source(&ui);
389 rebuild_reference_options(&ui, &displays, previous_reference);
390 update_reference_action(&ui, &displays);
391 let mut groups = callback_state.groups.lock().expect("group drafts poisoned");
392 let active = *callback_state
393 .active_group
394 .lock()
395 .expect("active group poisoned");
396 snapshot_group(&ui, &displays, &mut groups, active);
397 update_tab_projection(&ui, &groups, active);
398 project_group_telemetry(&ui, &groups, active);
399 persist_groups(&ui, &groups, active, &callback_state.profile_revision);
400 }
401 });
402
403 let flyout_weak = flyout.as_weak();
404 let callback_state = state.clone();
405 flyout.on_reference_selected(move || {
406 if let Some(ui) = flyout_weak.upgrade() {
407 let displays = callback_state
408 .displays
409 .lock()
410 .expect("display metadata poisoned");
411 update_reference_action(&ui, &displays);
412 if ui.get_stream_only() {
413 validate_stream_fields(&ui, &displays);
414 }
415 let mut groups = callback_state.groups.lock().expect("group drafts poisoned");
416 let active = *callback_state
417 .active_group
418 .lock()
419 .expect("active group poisoned");
420 snapshot_group(&ui, &displays, &mut groups, active);
421 update_tab_projection(&ui, &groups, active);
422 persist_groups(&ui, &groups, active, &callback_state.profile_revision);
423 }
424 });
425
426 let flyout_weak = flyout.as_weak();
427 let callback_state = state.clone();
428 flyout.on_strategy_selected(move || {
429 if let Some(ui) = flyout_weak.upgrade() {
430 let displays = callback_state
431 .displays
432 .lock()
433 .expect("display metadata poisoned");
434 if ui.get_page() == QuickAccessPage::Geometry {
435 update_geometry_preview(&ui, &displays);
436 } else {
437 if ui.get_stream_only() {
438 validate_stream_fields(&ui, &displays);
439 }
440 let mut groups = callback_state.groups.lock().expect("group drafts poisoned");
441 let active = *callback_state
442 .active_group
443 .lock()
444 .expect("active group poisoned");
445 snapshot_group(&ui, &displays, &mut groups, active);
446 update_tab_projection(&ui, &groups, active);
447 persist_groups(&ui, &groups, active, &callback_state.profile_revision);
448 }
449 }
450 });
451
452 let flyout_weak = flyout.as_weak();
453 let callback_state = state.clone();
454 flyout.on_calculate_geometry(move || {
455 if let Some(ui) = flyout_weak.upgrade() {
456 let displays = callback_state
457 .displays
458 .lock()
459 .expect("display metadata poisoned");
460 calculate_geometry(&ui, &displays);
461 if !ui.get_geometry_valid() {
462 return;
463 }
464 let mut groups = callback_state.groups.lock().expect("group drafts poisoned");
465 let active = *callback_state
466 .active_group
467 .lock()
468 .expect("active group poisoned");
469 snapshot_group(&ui, &displays, &mut groups, active);
470 update_tab_projection(&ui, &groups, active);
471 persist_groups(&ui, &groups, active, &callback_state.profile_revision);
472 }
473 });
474
475 let flyout_weak = flyout.as_weak();
476 let callback_displays = state.displays.clone();
477 flyout.on_open_manual_geometry(move || {
478 if let Some(ui) = flyout_weak.upgrade() {
479 ui.set_page(QuickAccessPage::Geometry);
480 let displays = callback_displays.lock().expect("display metadata poisoned");
481 update_geometry_preview(&ui, &displays);
482 reposition_after_layout(ui.as_weak());
483 }
484 });
485
486 let flyout_weak = flyout.as_weak();
487 flyout.on_close_geometry(move || {
488 if let Some(ui) = flyout_weak.upgrade() {
489 ui.set_page(QuickAccessPage::Main);
490 reposition_after_layout(ui.as_weak());
491 }
492 });
493
494 let flyout_weak = flyout.as_weak();
495 let callback_displays = state.displays.clone();
496 flyout.on_geometry_edited(move || {
497 if let Some(ui) = flyout_weak.upgrade() {
498 let displays = callback_displays.lock().expect("display metadata poisoned");
499 update_geometry_preview(&ui, &displays);
500 }
501 });
502
503 let flyout_weak = flyout.as_weak();
504 let callback_state = state.clone();
505 flyout.on_save_geometry(move || {
506 if let Some(ui) = flyout_weak.upgrade() {
507 let displays = callback_state
508 .displays
509 .lock()
510 .expect("display metadata poisoned");
511 calculate_geometry(&ui, &displays);
512 if !ui.get_geometry_valid() {
513 return;
514 }
515 let mut groups = callback_state.groups.lock().expect("group drafts poisoned");
516 let active = *callback_state
517 .active_group
518 .lock()
519 .expect("active group poisoned");
520 snapshot_group(&ui, &displays, &mut groups, active);
521 update_tab_projection(&ui, &groups, active);
522 if persist_groups(&ui, &groups, active, &callback_state.profile_revision) {
523 ui.set_page(QuickAccessPage::Main);
524 reposition_after_layout(ui.as_weak());
525 }
526 }
527 });
528}
529
530pub(super) fn register_stream_handlers(flyout: &QuickAccess, state: &HandlerState) {
531 let flyout_weak = flyout.as_weak();
532 let stream_displays = state.displays.clone();
533 flyout.on_open_stream_config(move || {
534 if let Some(ui) = flyout_weak.upgrade() {
535 update_stream_suggestions(&ui);
536 let displays = stream_displays.lock().expect("display metadata poisoned");
537 update_reference_action(&ui, &displays);
538 validate_stream_fields(&ui, &displays);
539 ui.set_page(QuickAccessPage::Stream);
540 reposition_after_layout(ui.as_weak());
541 }
542 });
543
544 let flyout_weak = flyout.as_weak();
545 flyout.on_close_stream_config(move || {
546 if let Some(ui) = flyout_weak.upgrade() {
547 ui.set_page(QuickAccessPage::Main);
548 reposition_after_layout(ui.as_weak());
549 }
550 });
551
552 let flyout_weak = flyout.as_weak();
553 let stream_displays = state.displays.clone();
554 flyout.on_stream_edited(move || {
555 if let Some(ui) = flyout_weak.upgrade() {
556 update_stream_suggestions(&ui);
557 let displays = stream_displays.lock().expect("display metadata poisoned");
558 update_reference_action(&ui, &displays);
559 validate_stream_fields(&ui, &displays);
560 }
561 });
562
563 let flyout_weak = flyout.as_weak();
564 let stream_state = state.clone();
565 flyout.on_save_stream_config(move || {
566 if let Some(ui) = flyout_weak.upgrade() {
567 let displays = stream_state
568 .displays
569 .lock()
570 .expect("display metadata poisoned");
571 if validate_stream_fields(&ui, &displays).is_none() {
572 return;
573 }
574 let mut groups = stream_state.groups.lock().expect("group drafts poisoned");
575 let active = *stream_state
576 .active_group
577 .lock()
578 .expect("active group poisoned");
579 snapshot_group(&ui, &displays, &mut groups, active);
580 update_tab_projection(&ui, &groups, active);
581 if persist_groups(&ui, &groups, active, &stream_state.profile_revision) {
582 ui.set_page(QuickAccessPage::Main);
583 reposition_after_layout(ui.as_weak());
584 }
585 }
586 });
587}
588
589pub(super) fn register_screen_size_handlers(
590 flyout: &QuickAccess,
591 sender: &ControllerSender,
592 state: &HandlerState,
593 override_store: &DisplayOverrideStore,
594) {
595 let flyout_weak = flyout.as_weak();
596 let screen_displays = state.displays.clone();
597 let screen_sender = sender.clone();
598 flyout.on_open_screen_size(move || {
599 if let Some(ui) = flyout_weak.upgrade() {
600 let displays = screen_displays.lock().expect("display metadata poisoned");
601 populate_screen_size_page(&ui, &displays);
602 ui.set_page(QuickAccessPage::ScreenSize);
603 screen_sender.refresh();
604 reposition_after_layout(ui.as_weak());
605 }
606 });
607
608 let flyout_weak = flyout.as_weak();
609 flyout.on_close_screen_size(move || {
610 if let Some(ui) = flyout_weak.upgrade() {
611 ui.set_page(QuickAccessPage::Main);
612 reposition_after_layout(ui.as_weak());
613 }
614 });
615
616 let flyout_weak = flyout.as_weak();
617 let screen_displays = state.displays.clone();
618 flyout.on_screen_size_edited(move || {
619 if let Some(ui) = flyout_weak.upgrade() {
620 let displays = screen_displays.lock().expect("display metadata poisoned");
621 update_screen_size_preview(&ui, &displays);
622 }
623 });
624
625 let flyout_weak = flyout.as_weak();
626 let screen_displays = state.displays.clone();
627 flyout.on_reset_screen_size(move || {
628 if let Some(ui) = flyout_weak.upgrade() {
629 ui.set_screen_size_manual_inches("".into());
630 let displays = screen_displays.lock().expect("display metadata poisoned");
631 update_screen_size_preview(&ui, &displays);
632 }
633 });
634
635 let reread_sender = sender.clone();
636 flyout.on_reread_screen_size(move || reread_sender.refresh());
637
638 let flyout_weak = flyout.as_weak();
639 let screen_state = state.clone();
640 let screen_store = override_store.clone();
641 flyout.on_save_screen_size(move || {
642 let Some(ui) = flyout_weak.upgrade() else {
643 return;
644 };
645 if ui.get_screen_size_save_blocked() {
646 return;
647 }
648 let mut displays = screen_state
649 .displays
650 .lock()
651 .expect("display metadata poisoned");
652 let Some(display_id) = selected_display_id(&ui, ui.get_selected_display()) else {
653 ui.set_screen_size_error("Target screen is no longer available".into());
654 return;
655 };
656 if display_id == STREAM_ONLY_ID {
657 return;
658 }
659 let manual = ui.get_screen_size_manual_inches();
660 let mut overrides = screen_state
661 .display_overrides
662 .lock()
663 .expect("display overrides poisoned");
664 let mut candidate = overrides.clone();
665 let candidate_result = if manual.trim().is_empty() {
666 candidate.remove(&display_id);
667 Ok(())
668 } else {
669 parse_screen_diagonal(manual.as_str()).and_then(|value| {
670 let aspect_ratio = screen_override_aspect_ratio(ui.get_screen_size_aspect_ratio())?;
671 candidate
672 .upsert(display_id.clone(), value, aspect_ratio)
673 .map_err(|error| error.to_string())
674 })
675 };
676 if let Err(error) = candidate_result.and_then(|_| {
677 persist_then_commit_display_overrides(&mut overrides, candidate, |candidate| {
678 screen_store
679 .save(candidate)
680 .map_err(|error| error.to_string())
681 })
682 }) {
683 ui.set_screen_size_error(error.into());
684 return;
685 }
686 apply_display_overrides(&mut displays, &overrides);
687 let mut groups = screen_state.groups.lock().expect("group drafts poisoned");
688 refresh_group_sizing(&mut groups, &displays);
689 refresh_group_tab_details(&mut groups, &displays);
690 let active = *screen_state
691 .active_group
692 .lock()
693 .expect("active group poisoned");
694 hydrate_group(&ui, &displays, &groups, active);
695 ui.set_page(QuickAccessPage::Main);
696 reposition_after_layout(ui.as_weak());
697 });
698}
699
700fn persist_then_commit_display_overrides(
701 current: &mut DisplayOverrides,
702 candidate: DisplayOverrides,
703 persist: impl FnOnce(&DisplayOverrides) -> Result<(), String>,
704) -> Result<(), String> {
705 persist(&candidate)?;
706 *current = candidate;
707 Ok(())
708}
709
710pub(super) fn register_group_handlers(flyout: &QuickAccess, state: &HandlerState) {
711 let flyout_weak = flyout.as_weak();
712 let tab_state = state.clone();
713 flyout.on_tab_selected(move |index| {
714 if let Some(ui) = flyout_weak.upgrade() {
715 switch_group(
716 &ui,
717 index,
718 &tab_state.groups,
719 &tab_state.active_group,
720 &tab_state.displays,
721 &tab_state.profile_revision,
722 );
723 }
724 });
725
726 let flyout_weak = flyout.as_weak();
727 let wheel_state = state.clone();
728 flyout.on_tab_wheel(move |delta| {
729 if let Some(ui) = flyout_weak.upgrade() {
730 let current = *wheel_state
731 .active_group
732 .lock()
733 .expect("active group poisoned");
734 let count = wheel_state
735 .groups
736 .lock()
737 .expect("group drafts poisoned")
738 .len();
739 let next = if delta > 0 {
740 (current + 1).min(count.saturating_sub(1))
741 } else {
742 current.saturating_sub(1)
743 };
744 switch_group(
745 &ui,
746 next as i32,
747 &wheel_state.groups,
748 &wheel_state.active_group,
749 &wheel_state.displays,
750 &wheel_state.profile_revision,
751 );
752 }
753 });
754
755 let flyout_weak = flyout.as_weak();
756 let add_state = state.clone();
757 flyout.on_add_group(move || {
758 if let Some(ui) = flyout_weak.upgrade() {
759 let displays = add_state
760 .displays
761 .lock()
762 .expect("display metadata poisoned");
763 let mut groups = add_state.groups.lock().expect("group drafts poisoned");
764 let current = *add_state
765 .active_group
766 .lock()
767 .expect("active group poisoned");
768 snapshot_group(&ui, &displays, &mut groups, current);
769 if groups.len() >= MAX_OUTPUTS {
770 return;
771 }
772 let id = (0..MAX_OUTPUTS as u32)
773 .find(|id| groups.iter().all(|group| group.id != *id))
774 .expect("mapping group capacity and ids diverged");
775 let mut group = GroupDraft::new(id);
776 group.stream_only = displays.iter().all(|display| {
777 groups.iter().any(|group| {
778 !group.stream_only && group.target_id.as_deref() == Some(display.id.as_str())
779 })
780 });
781 group.tab_detail = group_tab_detail(&group, &displays);
782 groups.push(group);
783 let next = groups.len() - 1;
784 *add_state
785 .active_group
786 .lock()
787 .expect("active group poisoned") = next;
788 hydrate_group(&ui, &displays, &groups, next);
789 persist_groups(&ui, &groups, next, &add_state.profile_revision);
790 }
791 });
792
793 let flyout_weak = flyout.as_weak();
794 let remove_state = state.clone();
795 flyout.on_remove_group(move |index| {
796 if let Some(ui) = flyout_weak.upgrade() {
797 let displays = remove_state
798 .displays
799 .lock()
800 .expect("display metadata poisoned");
801 let mut groups = remove_state.groups.lock().expect("group drafts poisoned");
802 if groups.len() <= 1 || index < 0 || index as usize >= groups.len() {
803 return;
804 }
805 let current = *remove_state
806 .active_group
807 .lock()
808 .expect("active group poisoned");
809 snapshot_group(&ui, &displays, &mut groups, current);
810 let removed = index as usize;
811 groups.remove(removed);
812 let next = active_after_removal(current, removed, groups.len());
813 *remove_state
814 .active_group
815 .lock()
816 .expect("active group poisoned") = next;
817 hydrate_group(&ui, &displays, &groups, next);
818 persist_groups(&ui, &groups, next, &remove_state.profile_revision);
819 }
820 });
821}
822
823pub(super) fn register_diagnostics_handlers(flyout: &QuickAccess) {
824 let flyout_weak = flyout.as_weak();
825 flyout.on_open_diagnostics(move || {
826 if let Some(ui) = flyout_weak.upgrade() {
827 populate_diagnostics(&ui);
828 ui.set_page(QuickAccessPage::Diagnostics);
829 reposition_after_layout(ui.as_weak());
830 }
831 });
832
833 let flyout_weak = flyout.as_weak();
834 flyout.on_close_diagnostics(move || {
835 if let Some(ui) = flyout_weak.upgrade() {
836 ui.set_page(QuickAccessPage::Main);
837 reposition_after_layout(ui.as_weak());
838 }
839 });
840
841 let flyout_weak = flyout.as_weak();
842 flyout.on_open_log_folder(move || {
843 if let Some(ui) = flyout_weak.upgrade() {
844 ui.set_diagnostic_action_error("".into());
845 if let Err(error) = open_log_folder() {
846 diagnostics::log(Level::Warn, "ui", "open-log-folder", None, error.as_str());
847 ui.set_diagnostic_action_error(error.into());
848 }
849 }
850 });
851}
852
853#[cfg(test)]
854mod tests {
855 use super::persist_then_commit_display_overrides;
856 use crate::config::DisplayOverrides;
857 use crate::geometry::AspectRatio;
858
859 fn overrides_with(display_id: &str, diagonal_inches: f64) -> DisplayOverrides {
860 let mut overrides = DisplayOverrides::default();
861 overrides
862 .upsert(
863 display_id.into(),
864 diagonal_inches,
865 Some(AspectRatio {
866 width: 16,
867 height: 9,
868 }),
869 )
870 .unwrap();
871 overrides
872 }
873
874 #[test]
875 fn failed_override_save_does_not_commit_candidate() {
876 let mut current = overrides_with("display-a", 24.0);
877 let original = current.clone();
878 let candidate = overrides_with("display-a", 27.0);
879
880 let result = persist_then_commit_display_overrides(&mut current, candidate, |_| {
881 Err("disk full".into())
882 });
883
884 assert_eq!(result, Err("disk full".into()));
885 assert_eq!(current, original);
886 }
887
888 #[test]
889 fn successful_override_save_commits_candidate() {
890 let mut current = overrides_with("display-a", 24.0);
891 let candidate = overrides_with("display-a", 27.0);
892 let expected = candidate.clone();
893
894 persist_then_commit_display_overrides(&mut current, candidate, |persisted| {
895 assert_eq!(persisted, &expected);
896 Ok(())
897 })
898 .unwrap();
899
900 assert_eq!(current, expected);
901 }
902}
903