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/state.rs13.2 KB
1use crate::config::ProfileRevision;
2use crate::config::{GroupConfig, GroupRouteConfig, ReferenceSource, StreamScreenConfig};
3use crate::geometry::{
4 AspectRatio, DisplayGeometry, PhysicalMeasurement, PixelSize, Rotation, SizingRequest,
5};
6use crate::limits::{
7 MAX_REFRESH_MILLIHZ, MILLIMETERS_PER_INCH, MIN_REFRESH_MILLIHZ, valid_refresh_millihz,
8};
9use crate::mapping::{MappingGroupRequest, MappingRoute};
10use crate::session_gate::VirtualMode;
11
12#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
13pub(super) enum SunshineState {
14 #[default]
15 Unavailable,
16 Starting,
17 Ready,
18 Failed,
19}
20
21impl 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)]
33pub(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)]
43pub(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
59pub(super) struct LoadedGroups {
60 pub groups: Vec<GroupDraft>,
61 pub active: usize,
62 pub profile: ProfileRevision,
63}
64
65#[derive(Clone, Copy, Debug)]
66pub(super) struct ValidatedStreamFields {
67 pub screen: StreamScreenConfig,
68}
69
70impl 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 None
88 } 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 = self
105 .screen
106 .diagonal_inches
107 .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)]
121pub(super) struct ValidatedGroupDraft {
122 id: u32,
123 reference_source: Option<ReferenceSource>,
124 sizing: Option<SizingRequest>,
125 route: ValidatedRoute,
126}
127
128#[derive(Clone)]
129enum 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
141impl ValidatedGroupDraft {
142 pub(super) fn id(&self) -> u32 {
143 self.id
144 }
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_id
171 .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 + 1
180 )
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
194impl 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 = screen
222 .diagonal_inches
223 .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 draft
233 }
234
235 pub(super) fn validate(&self) -> Result<ValidatedGroupDraft, String> {
236 let calculated = self
237 .sizing
238 .map(|request| {
239 request
240 .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 = calculated
254 .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.sizing
277 .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
301pub(super) fn parse_screen_diagonal(value: &str) -> Result<f64, String> {
302 let diagonal = value
303 .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
312pub(super) fn parse_stream_refresh_millihz(value: &str) -> Result<u32, String> {
313 let refresh = value
314 .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_000
330 ));
331 }
332 Ok(millihz)
333}
334
335pub(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 value
341 .trim_end_matches('0')
342 .trim_end_matches('.')
343 .to_string()
344 }
345}
346
347pub(super) fn parse_u32(value: &str, name: &str) -> Result<u32, String> {
348 value
349 .trim()
350 .parse()
351 .map_err(|_| format!("{name} must be a positive whole number"))
352}
353
354pub(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
367pub(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
377pub(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
386pub(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
396const 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
423pub(super) fn aspect_ratio_option_index(ratio: AspectRatio) -> i32 {
424 ASPECT_RATIOS
425 .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
434fn 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