1use crate::Theme;
7use egui::{Color32, Pos2, Rect, Sense, Stroke, Ui, Vec2};
8use egui_cha::ViewCtx;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum OutputType {
13 #[default]
14 Display,
15 NDI,
16 Record,
17 Stream,
18 Syphon,
19 Spout,
20 Virtual,
21}
22
23impl OutputType {
24 pub fn icon(&self) -> &'static str {
25 match self {
26 OutputType::Display => "🖥",
27 OutputType::NDI => "📡",
28 OutputType::Record => "⏺",
29 OutputType::Stream => "📺",
30 OutputType::Syphon => "🔗",
31 OutputType::Spout => "🔗",
32 OutputType::Virtual => "📦",
33 }
34 }
35
36 pub fn label(&self) -> &'static str {
37 match self {
38 OutputType::Display => "Display",
39 OutputType::NDI => "NDI",
40 OutputType::Record => "Record",
41 OutputType::Stream => "Stream",
42 OutputType::Syphon => "Syphon",
43 OutputType::Spout => "Spout",
44 OutputType::Virtual => "Virtual",
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum SourceType {
52 #[default]
53 Main,
54 Preview,
55 Layer,
56 Aux,
57}
58
59impl SourceType {
60 pub fn label(&self) -> &'static str {
61 match self {
62 SourceType::Main => "Main",
63 SourceType::Preview => "Preview",
64 SourceType::Layer => "Layer",
65 SourceType::Aux => "Aux",
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct RouteSource {
73 pub id: String,
74 pub name: String,
75 pub source_type: SourceType,
76 pub color: Option<Color32>,
77}
78
79impl RouteSource {
80 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
81 Self {
82 id: id.into(),
83 name: name.into(),
84 source_type: SourceType::Main,
85 color: None,
86 }
87 }
88
89 pub fn with_type(mut self, source_type: SourceType) -> Self {
90 self.source_type = source_type;
91 self
92 }
93
94 pub fn with_color(mut self, color: Color32) -> Self {
95 self.color = Some(color);
96 self
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct RouteOutput {
103 pub id: String,
104 pub name: String,
105 pub output_type: OutputType,
106 pub enabled: bool,
107 pub resolution: Option<(u32, u32)>,
108}
109
110impl RouteOutput {
111 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
112 Self {
113 id: id.into(),
114 name: name.into(),
115 output_type: OutputType::Display,
116 enabled: true,
117 resolution: None,
118 }
119 }
120
121 pub fn with_type(mut self, output_type: OutputType) -> Self {
122 self.output_type = output_type;
123 self
124 }
125
126 pub fn with_enabled(mut self, enabled: bool) -> Self {
127 self.enabled = enabled;
128 self
129 }
130
131 pub fn with_resolution(mut self, width: u32, height: u32) -> Self {
132 self.resolution = Some((width, height));
133 self
134 }
135}
136
137#[derive(Debug, Clone, PartialEq)]
139pub struct RouteConnection {
140 pub source_id: String,
141 pub output_id: String,
142}
143
144impl RouteConnection {
145 pub fn new(source_id: impl Into<String>, output_id: impl Into<String>) -> Self {
146 Self {
147 source_id: source_id.into(),
148 output_id: output_id.into(),
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
155pub enum RouterEvent {
156 Connect {
157 source_id: String,
158 output_id: String,
159 },
160 Disconnect {
161 source_id: String,
162 output_id: String,
163 },
164 ToggleOutput(String),
165 SelectSource(String),
166 SelectOutput(String),
167}
168
169pub struct OutputRouter<'a> {
171 sources: &'a [RouteSource],
172 outputs: &'a [RouteOutput],
173 connections: &'a [RouteConnection],
174 selected_source: Option<&'a str>,
175 selected_output: Option<&'a str>,
176 size: Vec2,
177 show_labels: bool,
178 show_resolution: bool,
179 compact: bool,
180}
181
182impl<'a> OutputRouter<'a> {
183 pub fn new(
184 sources: &'a [RouteSource],
185 outputs: &'a [RouteOutput],
186 connections: &'a [RouteConnection],
187 ) -> Self {
188 Self {
189 sources,
190 outputs,
191 connections,
192 selected_source: None,
193 selected_output: None,
194 size: Vec2::new(400.0, 200.0),
195 show_labels: true,
196 show_resolution: true,
197 compact: false,
198 }
199 }
200
201 pub fn selected_source(mut self, id: Option<&'a str>) -> Self {
202 self.selected_source = id;
203 self
204 }
205
206 pub fn selected_output(mut self, id: Option<&'a str>) -> Self {
207 self.selected_output = id;
208 self
209 }
210
211 pub fn size(mut self, width: f32, height: f32) -> Self {
212 self.size = Vec2::new(width, height);
213 self
214 }
215
216 pub fn show_labels(mut self, show: bool) -> Self {
217 self.show_labels = show;
218 self
219 }
220
221 pub fn show_resolution(mut self, show: bool) -> Self {
222 self.show_resolution = show;
223 self
224 }
225
226 pub fn compact(mut self, compact: bool) -> Self {
227 self.compact = compact;
228 self
229 }
230
231 pub fn show_with<Msg>(self, ctx: &mut ViewCtx<'_, Msg>, on_event: impl Fn(RouterEvent) -> Msg) {
232 if let Some(e) = self.show_internal(ctx.ui) {
233 ctx.emit(on_event(e));
234 }
235 }
236
237 pub fn show(self, ui: &mut Ui) -> Option<RouterEvent> {
238 self.show_internal(ui)
239 }
240
241 fn show_internal(self, ui: &mut Ui) -> Option<RouterEvent> {
242 let theme = Theme::current(ui.ctx());
243 let mut event: Option<RouterEvent> = None;
244
245 let (rect, _response) = ui.allocate_exact_size(self.size, Sense::hover());
246
247 if !ui.is_rect_visible(rect) {
248 return None;
249 }
250
251 let padding = theme.spacing_sm;
252 let inner_rect = rect.shrink(padding);
253
254 let source_width = if self.compact { 60.0 } else { 80.0 };
256 let output_height = if self.compact { 50.0 } else { 70.0 };
257 let node_size = if self.compact { 16.0 } else { 20.0 };
258
259 let matrix_rect = Rect::from_min_max(
260 Pos2::new(inner_rect.min.x + source_width + padding, inner_rect.min.y),
261 Pos2::new(inner_rect.max.x, inner_rect.max.y - output_height - padding),
262 );
263
264 struct SourceInfo {
266 id: String,
267 rect: Rect,
268 node_pos: Pos2,
269 clicked: bool,
270 hovered: bool,
271 }
272
273 struct OutputInfo {
274 id: String,
275 rect: Rect,
276 node_pos: Pos2,
277 clicked: bool,
278 hovered: bool,
279 toggle_clicked: bool,
280 }
281
282 struct MatrixNodeInfo {
283 source_id: String,
284 output_id: String,
285 pos: Pos2,
286 clicked: bool,
287 hovered: bool,
288 connected: bool,
289 }
290
291 let mut source_infos: Vec<SourceInfo> = Vec::new();
292 let mut output_infos: Vec<OutputInfo> = Vec::new();
293 let mut matrix_infos: Vec<MatrixNodeInfo> = Vec::new();
294
295 let source_spacing = if self.sources.is_empty() {
297 0.0
298 } else {
299 (matrix_rect.height() - node_size) / self.sources.len().max(1) as f32
300 };
301
302 for (i, source) in self.sources.iter().enumerate() {
303 let y = matrix_rect.min.y + node_size / 2.0 + i as f32 * source_spacing;
304 let source_rect = Rect::from_min_size(
305 Pos2::new(inner_rect.min.x, y - node_size),
306 Vec2::new(source_width, node_size * 2.0),
307 );
308 let node_pos = Pos2::new(matrix_rect.min.x, y);
309
310 let resp = ui.allocate_rect(source_rect, Sense::click());
311 source_infos.push(SourceInfo {
312 id: source.id.clone(),
313 rect: source_rect,
314 node_pos,
315 clicked: resp.clicked(),
316 hovered: resp.hovered(),
317 });
318 }
319
320 let output_spacing = if self.outputs.is_empty() {
322 0.0
323 } else {
324 (matrix_rect.width() - node_size) / self.outputs.len().max(1) as f32
325 };
326
327 for (i, output) in self.outputs.iter().enumerate() {
328 let x = matrix_rect.min.x + node_size / 2.0 + i as f32 * output_spacing;
329 let output_rect = Rect::from_min_size(
330 Pos2::new(x - node_size, inner_rect.max.y - output_height),
331 Vec2::new(node_size * 2.0 + 20.0, output_height),
332 );
333 let node_pos = Pos2::new(x, matrix_rect.max.y);
334
335 let resp = ui.allocate_rect(output_rect, Sense::click());
336
337 let toggle_rect = Rect::from_center_size(
339 Pos2::new(x, inner_rect.max.y - theme.spacing_sm - 8.0),
340 Vec2::splat(16.0),
341 );
342 let toggle_resp = ui.allocate_rect(toggle_rect, Sense::click());
343
344 output_infos.push(OutputInfo {
345 id: output.id.clone(),
346 rect: output_rect,
347 node_pos,
348 clicked: resp.clicked() && !toggle_resp.hovered(),
349 hovered: resp.hovered(),
350 toggle_clicked: toggle_resp.clicked(),
351 });
352 }
353
354 for (si, source) in self.sources.iter().enumerate() {
356 for (oi, output) in self.outputs.iter().enumerate() {
357 let y = matrix_rect.min.y + node_size / 2.0 + si as f32 * source_spacing;
358 let x = matrix_rect.min.x + node_size / 2.0 + oi as f32 * output_spacing;
359 let pos = Pos2::new(x, y);
360
361 let node_rect = Rect::from_center_size(pos, Vec2::splat(node_size + 4.0));
362 let resp = ui.allocate_rect(node_rect, Sense::click());
363
364 let connected = self
365 .connections
366 .iter()
367 .any(|c| c.source_id == source.id && c.output_id == output.id);
368
369 matrix_infos.push(MatrixNodeInfo {
370 source_id: source.id.clone(),
371 output_id: output.id.clone(),
372 pos,
373 clicked: resp.clicked(),
374 hovered: resp.hovered(),
375 connected,
376 });
377 }
378 }
379
380 let painter = ui.painter();
382
383 painter.rect_filled(rect, theme.radius_md, theme.bg_secondary);
385
386 painter.rect_filled(matrix_rect, theme.radius_sm, theme.bg_primary);
388
389 let grid_stroke = Stroke::new(0.5, theme.border.gamma_multiply(0.3));
391 for (i, _) in self.sources.iter().enumerate() {
392 let y = matrix_rect.min.y + node_size / 2.0 + i as f32 * source_spacing;
393 painter.line_segment(
394 [
395 Pos2::new(matrix_rect.min.x, y),
396 Pos2::new(matrix_rect.max.x, y),
397 ],
398 grid_stroke,
399 );
400 }
401 for (i, _) in self.outputs.iter().enumerate() {
402 let x = matrix_rect.min.x + node_size / 2.0 + i as f32 * output_spacing;
403 painter.line_segment(
404 [
405 Pos2::new(x, matrix_rect.min.y),
406 Pos2::new(x, matrix_rect.max.y),
407 ],
408 grid_stroke,
409 );
410 }
411
412 for info in matrix_infos.iter().filter(|i| i.connected) {
414 let source_info = source_infos.iter().find(|s| s.id == info.source_id);
415 let output_info = output_infos.iter().find(|o| o.id == info.output_id);
416
417 if let (Some(src), Some(out)) = (source_info, output_info) {
418 painter.line_segment(
420 [src.node_pos, Pos2::new(info.pos.x, src.node_pos.y)],
421 Stroke::new(2.0, theme.primary.gamma_multiply(0.5)),
422 );
423 painter.line_segment(
425 [info.pos, out.node_pos],
426 Stroke::new(2.0, theme.primary.gamma_multiply(0.5)),
427 );
428 }
429 }
430
431 for (info, source) in source_infos.iter().zip(self.sources.iter()) {
433 let is_selected = self.selected_source == Some(&source.id);
434 let is_hovered = info.hovered;
435
436 let color = source.color.unwrap_or(theme.primary);
437 let bg = if is_selected {
438 color.gamma_multiply(0.3)
439 } else if is_hovered {
440 theme.bg_tertiary
441 } else {
442 Color32::TRANSPARENT
443 };
444
445 painter.rect_filled(info.rect, theme.radius_sm, bg);
446
447 if self.show_labels {
449 painter.text(
450 Pos2::new(info.rect.min.x + theme.spacing_xs, info.node_pos.y),
451 egui::Align2::LEFT_CENTER,
452 &source.name,
453 egui::FontId::proportional(theme.font_size_xs),
454 theme.text_primary,
455 );
456 }
457
458 painter.text(
460 Pos2::new(
461 info.rect.min.x + theme.spacing_xs,
462 info.rect.max.y - theme.spacing_xs,
463 ),
464 egui::Align2::LEFT_BOTTOM,
465 source.source_type.label(),
466 egui::FontId::proportional(theme.font_size_xs * 0.8),
467 theme.text_muted,
468 );
469
470 painter.circle_filled(info.node_pos, node_size / 2.0 - 2.0, color);
472 painter.circle_stroke(
473 info.node_pos,
474 node_size / 2.0,
475 Stroke::new(1.0, theme.border),
476 );
477 }
478
479 for (info, output) in output_infos.iter().zip(self.outputs.iter()) {
481 let is_selected = self.selected_output == Some(&output.id);
482 let is_hovered = info.hovered;
483
484 let enabled_color = if output.enabled {
485 theme.primary
486 } else {
487 theme.text_muted
488 };
489 let bg = if is_selected {
490 enabled_color.gamma_multiply(0.3)
491 } else if is_hovered {
492 theme.bg_tertiary
493 } else {
494 Color32::TRANSPARENT
495 };
496
497 painter.rect_filled(info.rect, theme.radius_sm, bg);
498
499 painter.text(
501 Pos2::new(info.node_pos.x, info.rect.min.y + output_height * 0.3),
502 egui::Align2::CENTER_CENTER,
503 output.output_type.icon(),
504 egui::FontId::proportional(theme.font_size_md),
505 if output.enabled {
506 theme.text_primary
507 } else {
508 theme.text_muted
509 },
510 );
511
512 if self.show_labels {
514 painter.text(
515 Pos2::new(info.node_pos.x, info.rect.min.y + output_height * 0.55),
516 egui::Align2::CENTER_CENTER,
517 &output.name,
518 egui::FontId::proportional(theme.font_size_xs),
519 theme.text_secondary,
520 );
521 }
522
523 if self.show_resolution {
525 if let Some((w, h)) = output.resolution {
526 let res_text = format!("{}x{}", w, h);
527 painter.text(
528 Pos2::new(info.node_pos.x, info.rect.min.y + output_height * 0.75),
529 egui::Align2::CENTER_CENTER,
530 &res_text,
531 egui::FontId::proportional(theme.font_size_xs * 0.8),
532 theme.text_muted,
533 );
534 }
535 }
536
537 let toggle_rect = Rect::from_center_size(
539 Pos2::new(info.node_pos.x, inner_rect.max.y - theme.spacing_sm - 8.0),
540 Vec2::splat(14.0),
541 );
542 let toggle_bg = if output.enabled {
543 theme.state_success
544 } else {
545 theme.bg_tertiary
546 };
547 painter.rect_filled(toggle_rect, 2.0, toggle_bg);
548
549 painter.circle_filled(info.node_pos, node_size / 2.0 - 2.0, enabled_color);
551 painter.circle_stroke(
552 info.node_pos,
553 node_size / 2.0,
554 Stroke::new(1.0, theme.border),
555 );
556 }
557
558 for info in matrix_infos.iter() {
560 let fill = if info.connected {
561 theme.primary
562 } else if info.hovered {
563 theme.primary.gamma_multiply(0.5)
564 } else {
565 theme.bg_tertiary
566 };
567
568 painter.circle_filled(info.pos, node_size / 2.0 - 3.0, fill);
569 if info.connected || info.hovered {
570 painter.circle_stroke(
571 info.pos,
572 node_size / 2.0 - 1.0,
573 Stroke::new(1.5, theme.primary),
574 );
575 }
576 }
577
578 painter.rect_stroke(
580 rect,
581 theme.radius_md,
582 Stroke::new(theme.border_width, theme.border),
583 egui::StrokeKind::Inside,
584 );
585
586 for info in matrix_infos.iter() {
588 if info.clicked {
589 if info.connected {
590 event = Some(RouterEvent::Disconnect {
591 source_id: info.source_id.clone(),
592 output_id: info.output_id.clone(),
593 });
594 } else {
595 event = Some(RouterEvent::Connect {
596 source_id: info.source_id.clone(),
597 output_id: info.output_id.clone(),
598 });
599 }
600 break;
601 }
602 }
603
604 if event.is_none() {
605 for info in output_infos.iter() {
606 if info.toggle_clicked {
607 event = Some(RouterEvent::ToggleOutput(info.id.clone()));
608 break;
609 }
610 if info.clicked {
611 event = Some(RouterEvent::SelectOutput(info.id.clone()));
612 break;
613 }
614 }
615 }
616
617 if event.is_none() {
618 for info in source_infos.iter() {
619 if info.clicked {
620 event = Some(RouterEvent::SelectSource(info.id.clone()));
621 break;
622 }
623 }
624 }
625
626 event
627 }
628}