csi_webclient/ui/
stream.rs1use crate::state::{DeviceAction, DeviceState};
2
3const FRAME_LIST_HEIGHT: f32 = 220.0;
5
6pub fn render(ui: &mut egui::Ui, device: &mut DeviceState, actions: &mut Vec<DeviceAction>) {
11 ui.add(
12 egui::Label::new(format!("Stream — {}", device.id))
13 .wrap(),
14 );
15 ui.add_space(8.0);
16
17 ui.horizontal_wrapped(|ui| {
18 ui.checkbox(&mut device.auto_scroll_stream, "Auto-scroll");
19 ui.label(format!("Frames: {}", device.frames_received));
20 ui.label(format!("Bytes: {}", device.bytes_received));
21 });
22
23 render_recording_controls(ui, device, actions);
24
25 ui.add_space(8.0);
26 ui.strong("Recent frames");
27 ui.add_space(4.0);
28
29 egui::ScrollArea::vertical()
30 .id_salt("stream_frames_scroll")
31 .auto_shrink([false, false])
32 .max_height(FRAME_LIST_HEIGHT)
33 .stick_to_bottom(device.auto_scroll_stream)
34 .show(ui, |ui| {
35 ui.set_width(ui.available_width());
36 if device.recent_frames.is_empty() {
37 ui.label("No frames yet — connect the WebSocket and start collection.");
38 return;
39 }
40
41 ui.horizontal_wrapped(|ui| {
42 ui.strong("Time");
43 ui.label("·");
44 ui.strong("Len");
45 ui.label("·");
46 ui.strong("Preview (hex)");
47 });
48 ui.separator();
49
50 for frame in device.recent_frames.iter().rev().take(250) {
51 ui.horizontal_wrapped(|ui| {
52 ui.label(&frame.timestamp);
53 ui.label(format!("{} B", frame.length));
54 ui.add(
55 egui::Label::new(
56 egui::RichText::new(&frame.preview_hex).monospace(),
57 )
58 .wrap(),
59 );
60 });
61 ui.add_space(2.0);
62 }
63 });
64}
65
66pub fn render_export_dir(ui: &mut egui::Ui, export_dir: &mut String) {
68 ui.strong("Export to Parquet");
69 ui.horizontal_wrapped(|ui| {
70 ui.label("Output dir");
71 let field_w = (ui.available_width() - 80.0).clamp(120.0, 480.0);
72 ui.add(egui::TextEdit::singleline(export_dir).desired_width(field_w));
73 });
74}
75
76fn render_recording_controls(
78 ui: &mut egui::Ui,
79 device: &DeviceState,
80 actions: &mut Vec<DeviceAction>,
81) {
82 ui.add_space(6.0);
83 ui.strong("Recording");
84 ui.add_space(4.0);
85
86 let chip_known = device
87 .latest_info
88 .as_ref()
89 .and_then(|i| i.chip.as_deref())
90 .is_some();
91
92 ui.horizontal_wrapped(|ui| {
93 if device.recording {
94 if ui.button("⏹ Stop Recording").clicked() {
95 actions.push(DeviceAction::StopRecording);
96 }
97 ui.colored_label(
98 egui::Color32::from_rgb(220, 80, 80),
99 format!("● Recording — {} frame(s)", device.recorded_frames),
100 );
101 if device.record_decode_errors > 0 {
102 ui.label(format!("({} undecodable)", device.record_decode_errors));
103 }
104 } else {
105 let start = ui.add_enabled(chip_known, egui::Button::new("⏺ Start Recording"));
106 if start.clicked() {
107 actions.push(DeviceAction::StartRecording);
108 }
109 if !chip_known {
110 ui.label("— chip unknown; Fetch Info first");
111 } else if !device.ws_connected {
112 ui.label("— connect WebSocket to capture");
113 }
114 }
115 });
116
117 if let Some(path) = &device.record_path {
118 let label = if device.recording {
119 format!("Writing: {path}")
120 } else {
121 format!("Saved: {path}")
122 };
123 ui.add(egui::Label::new(label).wrap());
124 }
125}