Skip to main content

cu_bevymon/
lib.rs

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}