1mod focus;
2mod split;
3mod terminal;
4mod viewport;
5
6use bevy::asset::RenderAssetUsages;
7use bevy::input::mouse::MouseWheel;
8use bevy::input::{ButtonState, keyboard::KeyboardInput};
9use bevy::prelude::*;
10use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
11use bevy::window::PrimaryWindow;
12use cu_tuimon::{MonitorLogCapture, MonitorUi, MonitorUiAction, MonitorUiEvent, MonitorUiKey};
13use cu29::context::CuContext;
14use cu29::monitoring::{
15 ComponentId, CopperListIoStats, CopperListView, CuComponentState, CuMonitor,
16 CuMonitoringMetadata, CuMonitoringRuntime, Decision,
17};
18use cu29::{CuError, CuResult};
19
20pub use cu_tuimon::{MonitorModel, MonitorScreen, MonitorUiOptions, ScrollDirection};
21pub use focus::{CuBevyMonFocus, CuBevyMonFocusBorder, CuBevyMonSurface, CuBevyMonSurfaceNode};
22pub use split::{
23 CuBevyMonSplitLayoutConfig, CuBevyMonSplitLayoutEntities, CuBevyMonSplitMonitorPanel,
24 CuBevyMonSplitRoot, CuBevyMonSplitSimPanel, CuBevyMonSplitStyle, spawn_split_layout,
25};
26pub use terminal::CuBevyMonFontOptions;
27use terminal::{CuBevyMonTerminal, sync_terminal_to_panel};
28pub use viewport::CuBevyMonViewportSurface;
29
30pub struct CuBevyMon {
31 model: MonitorModel,
32 log_capture: Option<std::sync::Mutex<MonitorLogCapture>>,
33}
34
35impl CuBevyMon {
36 pub fn model(&self) -> MonitorModel {
37 self.model.clone()
38 }
39}
40
41impl CuMonitor for CuBevyMon {
42 fn new(metadata: CuMonitoringMetadata, _runtime: CuMonitoringRuntime) -> CuResult<Self> {
43 Ok(Self {
44 model: MonitorModel::from_metadata(&metadata),
45 log_capture: None,
46 })
47 }
48
49 fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
50 self.log_capture = Some(std::sync::Mutex::new(MonitorLogCapture::to_model(
51 self.model.clone(),
52 )));
53 Ok(())
54 }
55
56 fn process_copperlist(&self, ctx: &CuContext, view: CopperListView<'_>) -> CuResult<()> {
57 if let Some(log_capture) = &self.log_capture {
58 let mut log_capture = log_capture.lock().unwrap_or_else(|err| err.into_inner());
59 log_capture.poll();
60 }
61 self.model.process_copperlist(ctx.cl_id(), view);
62 Ok(())
63 }
64
65 fn observe_copperlist_io(&self, stats: CopperListIoStats) {
66 self.model.observe_copperlist_io(stats);
67 }
68
69 fn process_error(
70 &self,
71 component_id: ComponentId,
72 step: CuComponentState,
73 error: &CuError,
74 ) -> Decision {
75 self.model
76 .set_component_error(component_id, error.to_string());
77 match step {
78 CuComponentState::Start => Decision::Shutdown,
79 CuComponentState::Preprocess => Decision::Abort,
80 CuComponentState::Process => Decision::Ignore,
81 CuComponentState::Postprocess => Decision::Ignore,
82 CuComponentState::Stop => Decision::Shutdown,
83 }
84 }
85
86 fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
87 self.log_capture = None;
88 self.model.reset_latency();
89 Ok(())
90 }
91}
92
93#[derive(Resource, Clone)]
94pub struct CuBevyMonModel(pub MonitorModel);
95
96#[derive(Resource)]
97pub struct CuBevyMonUiState(pub MonitorUi);
98
99#[derive(Resource, Clone)]
100pub struct CuBevyMonTexture(pub Handle<Image>);
101
102#[derive(Component)]
103pub struct CuBevyMonPanel;
104
105pub struct CuBevyMonPlugin {
106 model: MonitorModel,
107 options: MonitorUiOptions,
108 font_options: CuBevyMonFontOptions,
109 initial_focus: CuBevyMonSurface,
110}
111
112impl CuBevyMonPlugin {
113 pub fn new(model: MonitorModel) -> Self {
114 Self {
115 model,
116 options: MonitorUiOptions::default(),
117 font_options: CuBevyMonFontOptions::default(),
118 initial_focus: CuBevyMonSurface::Monitor,
119 }
120 }
121
122 pub fn with_options(mut self, options: MonitorUiOptions) -> Self {
123 self.options = options;
124 self
125 }
126
127 pub fn with_initial_focus(mut self, initial_focus: CuBevyMonSurface) -> Self {
128 self.initial_focus = initial_focus;
129 self
130 }
131
132 pub fn with_font_options(mut self, font_options: CuBevyMonFontOptions) -> Self {
133 self.font_options = font_options;
134 self
135 }
136
137 pub fn with_font_size(mut self, size_px: u32) -> Self {
138 self.font_options = CuBevyMonFontOptions::new(size_px);
139 self
140 }
141}
142
143impl Plugin for CuBevyMonPlugin {
144 fn build(&self, app: &mut App) {
145 app.insert_resource(CuBevyMonModel(self.model.clone()))
146 .insert_resource(CuBevyMonUiState(MonitorUi::new(
147 self.model.clone(),
148 self.options.clone(),
149 )))
150 .insert_resource(self.font_options.clone())
151 .insert_resource(CuBevyMonFocus(self.initial_focus))
152 .add_systems(Startup, setup_terminal_context)
153 .add_systems(PostStartup, setup_terminal_texture)
154 .add_systems(
155 Update,
156 (
157 focus::update_surface_focus_from_click,
158 handle_monitor_pointer_input.after(focus::update_surface_focus_from_click),
159 handle_monitor_scroll_input,
160 handle_monitor_keyboard_input,
161 focus::update_surface_focus_borders,
162 draw_bevymon,
163 render_terminal_to_handle,
164 ),
165 )
166 .add_systems(
167 PostUpdate,
168 (
169 resize_terminal_to_panel,
170 viewport::sync_camera_viewports_to_surfaces,
171 ),
172 );
173 }
174}
175
176fn setup_terminal_context(
177 mut commands: Commands,
178 font_options: Res<CuBevyMonFontOptions>,
179) -> Result {
180 commands.insert_resource(CuBevyMonTerminal::from_options(&font_options)?);
181 Ok(())
182}
183
184fn setup_terminal_texture(
185 mut commands: Commands,
186 context: ResMut<CuBevyMonTerminal>,
187 mut images: ResMut<Assets<Image>>,
188) -> Result {
189 let width = context.backend().get_pixmap_width() as u32;
190 let height = context.backend().get_pixmap_height() as u32;
191 let data = context.backend().get_pixmap_data_as_rgba();
192
193 let image = Image::new(
194 Extent3d {
195 width,
196 height,
197 depth_or_array_layers: 1,
198 },
199 TextureDimension::D2,
200 data,
201 TextureFormat::Rgba8UnormSrgb,
202 RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD,
203 );
204 let handle = images.add(image);
205 commands.insert_resource(CuBevyMonTexture(handle));
206 Ok(())
207}
208
209fn draw_bevymon(
210 mut context: ResMut<CuBevyMonTerminal>,
211 mut ui_state: ResMut<CuBevyMonUiState>,
212) -> Result {
213 context.draw(|frame| {
214 ui_state.0.draw(frame);
215 })?;
216 Ok(())
217}
218
219fn render_terminal_to_handle(
220 context: ResMut<CuBevyMonTerminal>,
221 texture: Option<Res<CuBevyMonTexture>>,
222 mut images: ResMut<Assets<Image>>,
223) {
224 let Some(texture) = texture else {
225 return;
226 };
227
228 let width = context.backend().get_pixmap_width() as u32;
229 let height = context.backend().get_pixmap_height() as u32;
230 let Some(image) = images.get_mut(&texture.0) else {
231 return;
232 };
233
234 if image.width() != width || image.height() != height {
235 image.resize(Extent3d {
236 width,
237 height,
238 depth_or_array_layers: 1,
239 });
240 image.data = Some(context.backend().get_pixmap_data_as_rgba());
241 return;
242 }
243
244 let data_in = context.backend().get_pixmap_data();
245 let data_out = image.data.as_mut().expect("image data missing");
246 let (pixels_in, _) = data_in.as_chunks::<3>();
247 let (pixels_out, _) = data_out.as_chunks_mut::<4>();
248 for (px_in, px_out) in pixels_in.iter().zip(pixels_out.iter_mut()) {
249 px_out[0] = px_in[0];
250 px_out[1] = px_in[1];
251 px_out[2] = px_in[2];
252 px_out[3] = 255;
253 }
254}
255
256fn handle_monitor_pointer_input(
257 window: Single<&Window, With<PrimaryWindow>>,
258 mouse_buttons: Res<ButtonInput<MouseButton>>,
259 context: Res<CuBevyMonTerminal>,
260 mut ui_state: ResMut<CuBevyMonUiState>,
261 panels: Query<(&ComputedNode, &bevy::ui::UiGlobalTransform), With<CuBevyMonPanel>>,
262) {
263 if !mouse_buttons.just_pressed(MouseButton::Left)
264 && !mouse_buttons.just_released(MouseButton::Left)
265 {
266 return;
267 }
268
269 let Some((node, transform)) = panels.iter().next() else {
270 return;
271 };
272 let Some(local_point) = focus::local_cursor_position(&window, node, transform) else {
273 return;
274 };
275
276 let char_width = context.backend().char_width.max(1) as f32;
277 let char_height = context.backend().char_height.max(1) as f32;
278 let col = (local_point.x / char_width).floor().max(0.0) as u16;
279 let row = (local_point.y / char_height).floor().max(0.0) as u16;
280 let event = if mouse_buttons.just_pressed(MouseButton::Left) {
281 MonitorUiEvent::MouseDown { col, row }
282 } else {
283 MonitorUiEvent::MouseUp { col, row }
284 };
285 let _ = ui_state.0.handle_event(event);
286}
287
288fn handle_monitor_scroll_input(
289 focus: Res<CuBevyMonFocus>,
290 mut wheel_events: MessageReader<MouseWheel>,
291 mut ui_state: ResMut<CuBevyMonUiState>,
292) {
293 if focus.0 != CuBevyMonSurface::Monitor {
294 return;
295 }
296
297 for event in wheel_events.read() {
298 if event.y > 0.0 {
299 let _ = ui_state.0.handle_event(MonitorUiEvent::Scroll {
300 direction: ScrollDirection::Up,
301 steps: 1,
302 });
303 } else if event.y < 0.0 {
304 let _ = ui_state.0.handle_event(MonitorUiEvent::Scroll {
305 direction: ScrollDirection::Down,
306 steps: 1,
307 });
308 }
309
310 if event.x > 0.0 {
311 let _ = ui_state.0.handle_event(MonitorUiEvent::Scroll {
312 direction: ScrollDirection::Right,
313 steps: 5,
314 });
315 } else if event.x < 0.0 {
316 let _ = ui_state.0.handle_event(MonitorUiEvent::Scroll {
317 direction: ScrollDirection::Left,
318 steps: 5,
319 });
320 }
321 }
322}
323
324fn handle_monitor_keyboard_input(
325 focus: Res<CuBevyMonFocus>,
326 mut keyboard_inputs: MessageReader<KeyboardInput>,
327 mut exit: MessageWriter<AppExit>,
328 mut ui_state: ResMut<CuBevyMonUiState>,
329) {
330 if focus.0 != CuBevyMonSurface::Monitor {
331 return;
332 }
333
334 for event in keyboard_inputs.read() {
335 if event.state != ButtonState::Pressed {
336 continue;
337 }
338
339 if let Some(key) = monitor_navigation_key(event.key_code) {
340 dispatch_monitor_event(&mut ui_state.0, &mut exit, MonitorUiEvent::Key(key));
341 }
342
343 if let Some(text) = &event.text {
344 for ch in text.chars().filter(|ch| !ch.is_control()) {
345 dispatch_monitor_event(
346 &mut ui_state.0,
347 &mut exit,
348 MonitorUiEvent::Key(MonitorUiKey::Char(ch.to_ascii_lowercase())),
349 );
350 }
351 }
352 }
353}
354
355fn resize_terminal_to_panel(
356 mut context: ResMut<CuBevyMonTerminal>,
357 panels: Query<&ComputedNode, With<CuBevyMonPanel>>,
358) {
359 let Some(panel) = panels.iter().next() else {
360 return;
361 };
362 sync_terminal_to_panel(&mut context, panel.size());
363}
364
365fn dispatch_monitor_event(
366 ui_state: &mut MonitorUi,
367 exit: &mut MessageWriter<AppExit>,
368 event: MonitorUiEvent,
369) {
370 match ui_state.handle_event(event) {
371 MonitorUiAction::QuitRequested => {
372 exit.write(AppExit::Success);
373 }
374 MonitorUiAction::None => {}
375 MonitorUiAction::CopyLogSelection(_) => {}
376 }
377}
378
379fn monitor_navigation_key(key_code: KeyCode) -> Option<MonitorUiKey> {
380 match key_code {
381 KeyCode::ArrowLeft => Some(MonitorUiKey::Left),
382 KeyCode::ArrowRight => Some(MonitorUiKey::Right),
383 KeyCode::ArrowUp => Some(MonitorUiKey::Up),
384 KeyCode::ArrowDown => Some(MonitorUiKey::Down),
385 _ => None,
386 }
387}