From 512cf86eb930b0932cc0ff17270cf8ff2fa4a59e Mon Sep 17 00:00:00 2001 From: yatinmaan Date: Tue, 24 Dec 2024 18:12:26 +0530 Subject: [PATCH 1/4] Add scrolling --- src/log_viewer.rs | 98 ++++++++++++++++++++++++++++++++-- src/logging.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 221 insertions(+), 9 deletions(-) diff --git a/src/log_viewer.rs b/src/log_viewer.rs index e2dc126..adec907 100644 --- a/src/log_viewer.rs +++ b/src/log_viewer.rs @@ -7,6 +7,12 @@ pub const RENDER_LAYER: usize = 55; #[derive(Component)] pub(crate) struct LogViewerMarker; +#[derive(Component, PartialEq)] +pub(crate) enum ScrollState { + Auto, + Manual, +} + #[derive(Resource)] pub(crate) struct LogViewerState { pub(crate) visible: bool, @@ -18,6 +24,7 @@ pub(crate) struct LogViewerState { pub(crate) info_visible: bool, pub(crate) debug_visible: bool, pub(crate) trace_visible: bool, + pub(crate) scroll_state: ScrollState, } impl Default for LogViewerState { @@ -32,6 +39,7 @@ impl Default for LogViewerState { info_visible: true, debug_visible: true, trace_visible: true, + scroll_state: ScrollState::Auto, } } } @@ -39,6 +47,12 @@ impl Default for LogViewerState { #[derive(Component)] pub(crate) struct ListMarker; +#[derive(Component)] +pub(crate) struct ListContainerMarker; + +#[derive(Component)] +pub(crate) struct GoDownBtnMarker; + #[derive(Component)] pub(crate) enum TrafficLightButton { Red, @@ -79,9 +93,11 @@ pub fn setup_log_viewer_ui(mut commands: Commands, log_viewer_res: Res>, + mut scroll_positions: Query<&mut ScrollPosition, With>, + mut log_viewer_state: ResMut, +) { + if let Ok(mut scroll_position) = scroll_positions.get_mut(drag.entity()) { + scroll_position.offset_y -= drag.delta.y; + log_viewer_state.scroll_state = ScrollState::Manual; + } +} diff --git a/src/logging.rs b/src/logging.rs index 5650d44..f21feeb 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,17 +1,20 @@ use crate::{ debug_log_level::DebugLogLevel, log_viewer::{ - setup_log_viewer_ui, AutoCheckBox, ChipToggle, LevelFilterChip, ListMarker, - LogViewerMarker, LogViewerState, TrafficLightButton, RENDER_LAYER, + setup_log_viewer_ui, AutoCheckBox, ChipToggle, GoDownBtnMarker, LevelFilterChip, + ListContainerMarker, ListMarker, LogViewerMarker, LogViewerState, ScrollState, + TrafficLightButton, RENDER_LAYER, }, utils::{CheckboxIconMarker, ChipLeadingTextMarker}, }; use bevy::{ color::palettes::css, + input::mouse::{MouseScrollUnit, MouseWheel}, log::{ tracing_subscriber::{self, Layer}, BoxedLayer, }, + picking::focus::HoverMap, prelude::*, render::view::RenderLayers, utils::tracing::{self, level_filters::LevelFilter, Subscriber}, @@ -19,6 +22,8 @@ use bevy::{ use std::{num::NonZero, sync::mpsc}; use time::{format_description::well_known::iso8601, OffsetDateTime}; +const LOG_LINE_FONT_SIZE: f32 = 8.; + #[derive(Debug, Event, Clone)] struct LogEvent { message: String, @@ -26,6 +31,9 @@ struct LogEvent { timestamp: OffsetDateTime, } +#[derive(Debug, Event, Clone)] +struct ScrollToBottom; + #[derive(Deref, DerefMut)] struct LogEventsReceiver(mpsc::Receiver); @@ -105,6 +113,7 @@ impl Plugin for LogViewerPlugin { app.add_observer(handle_log_viewer_clear); app.add_observer(handle_auto_open_check); app.add_observer(handle_level_filter_chip_toggle); + app.add_observer(handle_scroll_to_bottom); app.add_systems(Startup, setup_log_viewer_ui); @@ -127,7 +136,10 @@ impl Plugin for LogViewerPlugin { // Running update_log_ui in PreUpdate to prevent data races between updating the UI and filtering log lines. // `handle_level_filter_chip_toggle`` can modify the `{level}_visible` fields in `LogViewerState` // while `update_log_ui` is adding new loglines to the viewer in parallel based on older values. - app.add_systems(PreUpdate, (receive_logs, update_log_counts)); + app.add_systems( + PreUpdate, + (manage_scroll_ui_state, receive_logs, update_log_counts).chain(), + ); app.add_systems( Update, @@ -135,6 +147,8 @@ impl Plugin for LogViewerPlugin { on_traffic_light_button, on_auto_open_check, on_level_filter_chip, + on_scroll_to_bottom_btn, + (handle_listcontainer_overflow, handle_scroll_update).chain(), ), ); } @@ -487,6 +501,9 @@ fn receive_logs( { commands.trigger(LogViewerVisibility::Show); } + if log_viewer_res.scroll_state == ScrollState::Auto { + commands.trigger(ScrollToBottom); + } } } } @@ -510,12 +527,83 @@ fn add_level_info( )); } +// Align the list to the End until it overflows, then switch to Default for scrolling to work. +fn handle_listcontainer_overflow( + mut commands: Commands, + mut scroll_query: Query<(&mut Node, &ComputedNode, &Children), With>, + child_comp_node_query: Query<&ComputedNode, With>, +) { + if let Ok((mut node, parent_comp_node, children)) = scroll_query.get_single_mut() { + if let Ok(child_comp_node) = child_comp_node_query.get(children[0]) { + let overflown = parent_comp_node.size().y < child_comp_node.size().y; + if !overflown && node.align_items != AlignItems::End { + node.align_items = AlignItems::End; + } else if overflown && node.align_items != AlignItems::Default { + node.align_items = AlignItems::Default; + commands.trigger(ScrollToBottom); + } + } + } +} + +fn manage_scroll_ui_state( + mut log_viewer: ResMut, + mut border_color_q: Query<&mut BorderColor, With>, + mut scroll_to_bottom_btn_q: Query<&mut Node, With>, + mut scroll_query: Query<(&ScrollPosition, &ComputedNode, &Children), With>, + child_comp_node_query: Query<&ComputedNode, With>, +) { + if let Ok((scroll_position, parent_comp_node, children)) = scroll_query.get_single_mut() { + if let Ok(child_comp_node) = child_comp_node_query.get(children[0]) { + // The list is at bottom if the sum of the parent's height and the scroll offset is equal to the child's height. + // We subtract the font size to account for the last log line being partially visible and still count that as being at the bottom. + let is_at_bottom = parent_comp_node.size().y + scroll_position.offset_y + >= child_comp_node.size().y - LOG_LINE_FONT_SIZE; + log_viewer.scroll_state = if is_at_bottom { + if let Ok(mut border_color) = border_color_q.get_single_mut() { + *border_color = Color::NONE.into(); + } + if let Ok(mut scroll_to_bottom_btn) = scroll_to_bottom_btn_q.get_single_mut() { + scroll_to_bottom_btn.display = Display::None; + } + ScrollState::Auto + } else { + if let Ok(mut border_color) = border_color_q.get_single_mut() { + *border_color = css::WHITE.with_alpha(0.25).into(); + } + if let Ok(mut scroll_to_bottom_btn) = scroll_to_bottom_btn_q.get_single_mut() { + scroll_to_bottom_btn.display = Display::Flex; + } + ScrollState::Manual + }; + } + } +} + +fn handle_scroll_to_bottom( + _trigger: Trigger, + mut log_viewer: ResMut, + mut scroll_query: Query<(&mut ScrollPosition, &Children), With>, + computed_node_query: Query<&ComputedNode, With>, +) { + // ListContainerMarker -> ListMarker have a Parent -> Child relationship. + if let Ok((mut scroll_position, children)) = scroll_query.get_single_mut() { + if let Ok(computed_node) = computed_node_query.get(children[0]) { + scroll_position.offset_y = computed_node.size().y; + log_viewer.scroll_state = ScrollState::Auto; + } + } +} + fn spawn_logline(commands: &mut Commands, parent: Entity, event: &LogEvent) -> Entity { let dbg_level = DebugLogLevel::from(*event.metadata.level()); - const LOG_LINE_FONT_SIZE: f32 = 8.; commands .spawn(( + PickingBehavior { + should_block_lower: false, + ..default() + }, TextLayout::default().with_linebreak(LineBreak::AnyCharacter), Text::default(), // Label, @@ -594,3 +682,39 @@ fn on_level_filter_chip( } } } + +fn on_scroll_to_bottom_btn( + mut interaction_query: Query<&Interaction, (Changed, With)>, + mut commands: Commands, +) { + for interaction in &mut interaction_query { + if matches!(*interaction, Interaction::Pressed) { + commands.trigger(ScrollToBottom); + } + } +} + +fn handle_scroll_update( + mut mouse_wheel_events: EventReader, + hover_map: Res, + mut scrolled_node_query: Query<&mut ScrollPosition, With>, +) { + for mouse_wheel_event in mouse_wheel_events.read() { + let (dx, dy) = match mouse_wheel_event.unit { + MouseScrollUnit::Line => ( + mouse_wheel_event.x * LOG_LINE_FONT_SIZE, + mouse_wheel_event.y * LOG_LINE_FONT_SIZE, + ), + MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y), + }; + + for (_pointer, pointer_map) in hover_map.iter() { + for (entity, _hit) in pointer_map.iter() { + if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) { + scroll_position.offset_x -= dx; + scroll_position.offset_y -= dy; + } + } + } + } +} From c2dc4fe7352f9197812b47598a09d918f67bcbed Mon Sep 17 00:00:00 2001 From: extrawurst Date: Tue, 7 Jan 2025 11:39:18 +0100 Subject: [PATCH 2/4] smaller scroll down button --- src/log_viewer.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/log_viewer.rs b/src/log_viewer.rs index adec907..f0477b3 100644 --- a/src/log_viewer.rs +++ b/src/log_viewer.rs @@ -266,6 +266,8 @@ pub fn setup_log_viewer_ui(mut commands: Commands, log_viewer_res: Res Date: Tue, 7 Jan 2025 11:52:12 +0100 Subject: [PATCH 3/4] cleanup --- src/log_viewer.rs | 5 ++++- src/logging.rs | 42 +++++++++++++++++------------------------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/log_viewer.rs b/src/log_viewer.rs index f0477b3..cd71ab7 100644 --- a/src/log_viewer.rs +++ b/src/log_viewer.rs @@ -1,6 +1,6 @@ use bevy::{prelude::*, render::view::RenderLayers, utils::tracing::level_filters::LevelFilter}; -use crate::{debug_log_level::DebugLogLevel, utils}; +use crate::{debug_log_level::DebugLogLevel, utils, ScrollToBottom}; pub const RENDER_LAYER: usize = 55; @@ -290,6 +290,9 @@ pub fn setup_log_viewer_ui(mut commands: Commands, log_viewer_res: Res>, mut commands: Commands| { + commands.trigger(ScrollToBottom); + }) .with_children(|parent| { // Create a down-arrow icon by rotating a square 45 degrees // and clipping the overflow at the top. diff --git a/src/logging.rs b/src/logging.rs index f21feeb..06017c2 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -32,7 +32,7 @@ struct LogEvent { } #[derive(Debug, Event, Clone)] -struct ScrollToBottom; +pub(crate) struct ScrollToBottom; #[derive(Deref, DerefMut)] struct LogEventsReceiver(mpsc::Receiver); @@ -147,7 +147,6 @@ impl Plugin for LogViewerPlugin { on_traffic_light_button, on_auto_open_check, on_level_filter_chip, - on_scroll_to_bottom_btn, (handle_listcontainer_overflow, handle_scroll_update).chain(), ), ); @@ -559,21 +558,25 @@ fn manage_scroll_ui_state( // We subtract the font size to account for the last log line being partially visible and still count that as being at the bottom. let is_at_bottom = parent_comp_node.size().y + scroll_position.offset_y >= child_comp_node.size().y - LOG_LINE_FONT_SIZE; + + if let Ok(mut border_color) = border_color_q.get_single_mut() { + *border_color = if is_at_bottom { + Color::NONE.into() + } else { + css::WHITE.with_alpha(0.25).into() + }; + } + if let Ok(mut scroll_to_bottom_btn) = scroll_to_bottom_btn_q.get_single_mut() { + scroll_to_bottom_btn.display = if is_at_bottom { + Display::None + } else { + Display::Flex + }; + } + log_viewer.scroll_state = if is_at_bottom { - if let Ok(mut border_color) = border_color_q.get_single_mut() { - *border_color = Color::NONE.into(); - } - if let Ok(mut scroll_to_bottom_btn) = scroll_to_bottom_btn_q.get_single_mut() { - scroll_to_bottom_btn.display = Display::None; - } ScrollState::Auto } else { - if let Ok(mut border_color) = border_color_q.get_single_mut() { - *border_color = css::WHITE.with_alpha(0.25).into(); - } - if let Ok(mut scroll_to_bottom_btn) = scroll_to_bottom_btn_q.get_single_mut() { - scroll_to_bottom_btn.display = Display::Flex; - } ScrollState::Manual }; } @@ -683,17 +686,6 @@ fn on_level_filter_chip( } } -fn on_scroll_to_bottom_btn( - mut interaction_query: Query<&Interaction, (Changed, With)>, - mut commands: Commands, -) { - for interaction in &mut interaction_query { - if matches!(*interaction, Interaction::Pressed) { - commands.trigger(ScrollToBottom); - } - } -} - fn handle_scroll_update( mut mouse_wheel_events: EventReader, hover_map: Res, From 411ede95e946c0ef960e73ceeb310f83d40b2d8c Mon Sep 17 00:00:00 2001 From: extrawurst Date: Tue, 7 Jan 2025 11:55:10 +0100 Subject: [PATCH 4/4] added changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 133dafc..73faa37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +* allow scrolling + ## [0.4.2] - 2024-12-14 ### Changed