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/state.rs13.2 KB
1
use crate::config::ProfileRevision;2
use crate::config::{GroupConfig, GroupRouteConfig, ReferenceSource, StreamScreenConfig};3
use crate::geometry::{4
AspectRatio, DisplayGeometry, PhysicalMeasurement, PixelSize, Rotation, SizingRequest,5
};6
use crate::limits::{7
MAX_REFRESH_MILLIHZ, MILLIMETERS_PER_INCH, MIN_REFRESH_MILLIHZ, valid_refresh_millihz,8
};9
use crate::mapping::{MappingGroupRequest, MappingRoute};10
use crate::session_gate::VirtualMode;11
12
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]13
pub(super) enum SunshineState {14
#[default]15
Unavailable,16
Starting,17
Ready,18
Failed,19
}20
21
impl SunshineState {22
pub(super) fn as_i32(self) -> i32 {23
match self {24
Self::Unavailable => 0,25
Self::Starting => 1,26
Self::Ready => 2,27
Self::Failed => 3,28
}29
}30
}31
32
#[derive(Clone, Debug, Default)]33
pub(super) struct GroupTelemetry {34
pub sunshine_id: Option<String>,35
pub sunshine_port: Option<u16>,36
pub sunshine_state: SunshineState,37
pub fps: Option<u32>,38
pub ready: bool,39
pub error: bool,40
}41
42
#[derive(Clone, Debug)]43
pub(super) struct GroupDraft {44
pub id: u32,45
pub target_id: Option<String>,46
pub stream_only: bool,47
pub reference_source: Option<ReferenceSource>,48
pub sizing: Option<SizingRequest>,49
pub stream_width: String,50
pub stream_height: String,51
pub stream_diagonal: String,52
pub stream_refresh: String,53
pub stream_aspect_ratio_index: i32,54
pub stream_rotation_index: i32,55
pub telemetry: GroupTelemetry,56
pub tab_detail: String,57
}58
59
pub(super) struct LoadedGroups {60
pub groups: Vec<GroupDraft>,61
pub active: usize,62
pub profile: ProfileRevision,63
}64
65
#[derive(Clone, Copy, Debug)]66
pub(super) struct ValidatedStreamFields {67
pub screen: StreamScreenConfig,68
}69
70
impl ValidatedStreamFields {71
pub(super) fn parse(72
width: &str,73
height: &str,74
diagonal: &str,75
refresh: &str,76
aspect_ratio_index: i32,77
rotation_index: i32,78
) -> Result<Self, String> {79
let width = parse_u32(width, "Streaming screen width")?;80
let height = parse_u32(height, "Streaming screen height")?;81
let refresh_millihz = parse_stream_refresh_millihz(refresh)?;82
VirtualMode::from_millihz(width, height, refresh_millihz)83
.map_err(|error| error.to_string())?;84
85
let diagonal = diagonal.trim();86
let diagonal_inches = if diagonal.is_empty() {87
None88
} else {89
Some(parse_screen_diagonal(diagonal)?)90
};91
Ok(Self {92
screen: StreamScreenConfig {93
width,94
height,95
diagonal_inches,96
refresh_millihz,97
aspect_ratio: aspect_ratio_from_index(aspect_ratio_index)?,98
rotation: rotation_from_index(rotation_index)?,99
},100
})101
}102
103
pub(super) fn target_geometry(self) -> Result<DisplayGeometry, String> {104
let diagonal_inches = self105
.screen106
.diagonal_inches107
.ok_or_else(|| "Enter a valid diagonal in inches".to_string())?;108
Ok(DisplayGeometry {109
native_pixels: PixelSize {110
width: self.screen.width,111
height: self.screen.height,112
},113
physical: PhysicalMeasurement::DiagonalMm(diagonal_inches * MILLIMETERS_PER_INCH),114
aspect_ratio: Some(self.screen.aspect_ratio),115
rotation: self.screen.rotation,116
})117
}118
}119
120
#[derive(Clone)]121
pub(super) struct ValidatedGroupDraft {122
id: u32,123
reference_source: Option<ReferenceSource>,124
sizing: Option<SizingRequest>,125
route: ValidatedRoute,126
}127
128
#[derive(Clone)]129
enum ValidatedRoute {130
Mirror {131
target_id: Option<String>,132
mode: VirtualMode,133
rotation: Rotation,134
},135
StreamOnly {136
screen: StreamScreenConfig,137
mode: Option<VirtualMode>,138
},139
}140
141
impl ValidatedGroupDraft {142
pub(super) fn id(&self) -> u32 {143
self.id144
}145
146
pub(super) fn to_config(&self) -> GroupConfig {147
let route = match &self.route {148
ValidatedRoute::Mirror { target_id, .. } => GroupRouteConfig::Mirror {149
target_id: target_id.clone(),150
},151
ValidatedRoute::StreamOnly { screen, .. } => {152
GroupRouteConfig::StreamOnly { screen: *screen }153
}154
};155
GroupConfig {156
id: self.id,157
route,158
reference_source: self.reference_source.clone(),159
sizing: self.sizing,160
}161
}162
163
pub(super) fn to_mapping_request(&self) -> Result<MappingGroupRequest, String> {164
let (mode, rotation, route) = match &self.route {165
ValidatedRoute::Mirror {166
target_id,167
mode,168
rotation,169
} => {170
let target = target_id171
.clone()172
.ok_or_else(|| format!("Choose a target screen for Output {}", self.id + 1))?;173
(*mode, *rotation, MappingRoute::Mirror { target })174
}175
ValidatedRoute::StreamOnly { mode, screen } => {176
let mode = (*mode).ok_or_else(|| {177
format!(178
"Configure streaming screen geometry and a reference display for Output {}",179
self.id + 1180
)181
})?;182
(mode, screen.rotation, MappingRoute::StreamOnly)183
}184
};185
Ok(MappingGroupRequest {186
id: self.id,187
mode,188
rotation,189
route,190
})191
}192
}193
194
impl GroupDraft {195
pub(super) fn new(id: u32) -> Self {196
Self {197
id,198
target_id: None,199
stream_only: false,200
reference_source: None,201
sizing: None,202
stream_width: "3840".into(),203
stream_height: "2160".into(),204
stream_diagonal: String::new(),205
stream_refresh: "60".into(),206
stream_aspect_ratio_index: 0,207
stream_rotation_index: 0,208
telemetry: GroupTelemetry::default(),209
tab_detail: "—".into(),210
}211
}212
213
pub(super) fn from_config(config: &GroupConfig) -> Self {214
let mut draft = Self::new(config.id);215
match &config.route {216
GroupRouteConfig::Mirror { target_id } => draft.target_id = target_id.clone(),217
GroupRouteConfig::StreamOnly { screen } => {218
draft.stream_only = true;219
draft.stream_width = screen.width.to_string();220
draft.stream_height = screen.height.to_string();221
draft.stream_diagonal = screen222
.diagonal_inches223
.map(|value| value.to_string())224
.unwrap_or_default();225
draft.stream_refresh = format_refresh_input(screen.refresh_millihz);226
draft.stream_aspect_ratio_index = aspect_ratio_option_index(screen.aspect_ratio);227
draft.stream_rotation_index = rotation_index(screen.rotation);228
}229
}230
draft.reference_source = config.reference_source.clone();231
draft.sizing = config.sizing;232
draft233
}234
235
pub(super) fn validate(&self) -> Result<ValidatedGroupDraft, String> {236
let calculated = self237
.sizing238
.map(|request| {239
request240
.calculate()241
.map_err(|error| format!("Output {} geometry: {error}", self.id + 1))242
})243
.transpose()?;244
let route = if self.stream_only {245
let stream = ValidatedStreamFields::parse(246
&self.stream_width,247
&self.stream_height,248
&self.stream_diagonal,249
&self.stream_refresh,250
self.stream_aspect_ratio_index,251
self.stream_rotation_index,252
)?;253
let mode = calculated254
.map(|result| {255
VirtualMode::from_millihz(256
result.virtual_mode.width,257
result.virtual_mode.height,258
stream.screen.refresh_millihz,259
)260
.map_err(|error| format!("Output {}: {error}", self.id + 1))261
})262
.transpose()?;263
ValidatedRoute::StreamOnly {264
screen: stream.screen,265
mode,266
}267
} else {268
let (mode, rotation) = match calculated {269
Some(result) => (270
VirtualMode::from_millihz(271
result.virtual_mode.width,272
result.virtual_mode.height,273
result.preferred_refresh_millihz.unwrap_or(240_000),274
)275
.map_err(|error| format!("Output {}: {error}", self.id + 1))?,276
self.sizing277
.map(|request| request.target.rotation)278
.unwrap_or(Rotation::Deg0),279
),280
None => (VirtualMode::default(), Rotation::Deg0),281
};282
ValidatedRoute::Mirror {283
target_id: self.target_id.clone(),284
mode,285
rotation,286
}287
};288
Ok(ValidatedGroupDraft {289
id: self.id,290
reference_source: self.reference_source.clone(),291
sizing: self.sizing,292
route,293
})294
}295
296
pub(super) fn reset_telemetry(&mut self) {297
self.telemetry = GroupTelemetry::default();298
}299
}300
301
pub(super) fn parse_screen_diagonal(value: &str) -> Result<f64, String> {302
let diagonal = value303
.trim()304
.parse::<f64>()305
.map_err(|_| "Enter a valid diagonal in inches".to_string())?;306
if !crate::limits::valid_physical_millimeters(diagonal * MILLIMETERS_PER_INCH) {307
return Err("Diagonal must be between 0.4 and 393.7 inches".into());308
}309
Ok(diagonal)310
}311
312
pub(super) fn parse_stream_refresh_millihz(value: &str) -> Result<u32, String> {313
let refresh = value314
.trim()315
.parse::<f64>()316
.map_err(|_| "Refresh rate must be a positive number".to_string())?;317
if !refresh.is_finite() || refresh <= 0.0 {318
return Err("Refresh rate must be a positive number".into());319
}320
let millihz = (refresh * 1_000.0).round();321
if millihz > u32::MAX as f64 {322
return Err("Refresh rate is too large".into());323
}324
let millihz = millihz as u32;325
if !valid_refresh_millihz(millihz) {326
return Err(format!(327
"Refresh rate must be between {} and {} Hz",328
MIN_REFRESH_MILLIHZ / 1_000,329
MAX_REFRESH_MILLIHZ / 1_000330
));331
}332
Ok(millihz)333
}334
335
pub(super) fn format_refresh_input(refresh_millihz: u32) -> String {336
if refresh_millihz.is_multiple_of(1_000) {337
(refresh_millihz / 1_000).to_string()338
} else {339
let value = format!("{:.3}", f64::from(refresh_millihz) / 1_000.0);340
value341
.trim_end_matches('0')342
.trim_end_matches('.')343
.to_string()344
}345
}346
347
pub(super) fn parse_u32(value: &str, name: &str) -> Result<u32, String> {348
value349
.trim()350
.parse()351
.map_err(|_| format!("{name} must be a positive whole number"))352
}353
354
pub(super) fn aspect_ratio_from_index(index: i32) -> Result<AspectRatio, String> {355
let (width, height) = match index {356
0 => (16, 9),357
1 => (16, 10),358
2 => (3, 2),359
3 => (4, 3),360
4 => (21, 9),361
5 => (32, 9),362
_ => return Err("Choose a supported aspect ratio".into()),363
};364
Ok(AspectRatio { width, height })365
}366
367
pub(super) fn rotation_from_index(index: i32) -> Result<Rotation, String> {368
match index {369
0 => Ok(Rotation::Deg0),370
1 => Ok(Rotation::Deg180),371
2 => Ok(Rotation::Deg90),372
3 => Ok(Rotation::Deg270),373
_ => Err("Choose a supported display orientation".into()),374
}375
}376
377
pub(super) fn rotation_index(rotation: Rotation) -> i32 {378
match rotation {379
Rotation::Deg0 => 0,380
Rotation::Deg180 => 1,381
Rotation::Deg90 => 2,382
Rotation::Deg270 => 3,383
}384
}385
386
pub(super) fn rotation_label(index: i32) -> Option<&'static str> {387
match index {388
0 => Some("Landscape"),389
1 => Some("Landscape flipped"),390
2 => Some("Portrait clockwise"),391
3 => Some("Portrait counter-clockwise"),392
_ => None,393
}394
}395
396
const ASPECT_RATIOS: [AspectRatio; 6] = [397
AspectRatio {398
width: 16,399
height: 9,400
},401
AspectRatio {402
width: 16,403
height: 10,404
},405
AspectRatio {406
width: 3,407
height: 2,408
},409
AspectRatio {410
width: 4,411
height: 3,412
},413
AspectRatio {414
width: 21,415
height: 9,416
},417
AspectRatio {418
width: 32,419
height: 9,420
},421
];422
423
pub(super) fn aspect_ratio_option_index(ratio: AspectRatio) -> i32 {424
ASPECT_RATIOS425
.iter()426
.enumerate()427
.min_by(|(_, left), (_, right)| {428
aspect_ratio_error(ratio, **left).total_cmp(&aspect_ratio_error(ratio, **right))429
})430
.map(|(index, _)| index as i32)431
.unwrap_or(0)432
}433
434
fn aspect_ratio_error(left: AspectRatio, right: AspectRatio) -> f64 {435
(left.width as f64 / left.height as f64 - right.width as f64 / right.height as f64).abs()436
}437