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.
文件
../ diagnostic_view.rs6.1 KBgeometry_view.rs15.9 KBhandlers.rs32.1 KBprojection.rs6.7 KBstate.rs13.2 KBsrc/ui/handlers.rs32.1 KB
1
use std::sync::atomic::AtomicU64;2
use std::sync::{Arc, Mutex};3
use std::thread;4
5
use crate::config::{ConfigProfiles, DisplayOverrideStore, DisplayOverrides};6
use crate::controller::{ControllerSender, DisplayOption};7
use crate::diagnostics::{self, Level};8
use crate::limits::MAX_OUTPUTS;9
10
use super::*;11
12
#[derive(Clone)]13
pub(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
21
impl 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
39
pub(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_state58
.displays59
.lock()60
.expect("display metadata poisoned");61
let mut groups = start_state.groups.lock().expect("group drafts poisoned");62
let active = *start_state63
.active_group64
.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_state125
.active_group126
.lock()127
.expect("active group poisoned");128
let Some(port) = groups129
.get(active)130
.filter(|group| {131
group.stream_only132
&& group.telemetry.ready133
&& group.telemetry.sunshine_state == SunshineState::Ready134
})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
158
pub(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, _) = store178
.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
*state185
.profile_revision186
.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) = store206
.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
*state214
.profile_revision215
.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 = store243
.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
302
fn spawn_profile_write<T, Operation, Completion>(303
ui: slint::Weak<QuickAccess>,304
action: &'static str,305
operation: Operation,306
completion: Completion,307
) where308
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
353
fn 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
377
pub(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_state383
.displays384
.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_state393
.active_group394
.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_state408
.displays409
.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_state417
.active_group418
.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_state431
.displays432
.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_state442
.active_group443
.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_state457
.displays458
.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_state466
.active_group467
.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_state508
.displays509
.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_state517
.active_group518
.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
530
pub(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_state568
.displays569
.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_state576
.active_group577
.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
589
pub(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_state649
.displays650
.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_state661
.display_overrides662
.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
candidate672
.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_store679
.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_state691
.active_group692
.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
700
fn 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
710
pub(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_state731
.active_group732
.lock()733
.expect("active group poisoned");734
let count = wheel_state735
.groups736
.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_state760
.displays761
.lock()762
.expect("display metadata poisoned");763
let mut groups = add_state.groups.lock().expect("group drafts poisoned");764
let current = *add_state765
.active_group766
.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_state785
.active_group786
.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_state798
.displays799
.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_state806
.active_group807
.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_state814
.active_group815
.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
823
pub(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)]854
mod 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
overrides862
.upsert(863
display_id.into(),864
diagonal_inches,865
Some(AspectRatio {866
width: 16,867
height: 9,868
}),869
)870
.unwrap();871
overrides872
}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