Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 99 additions & 6 deletions src/log_viewer.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
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;

#[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,
Expand All @@ -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 {
Expand All @@ -32,13 +39,20 @@ impl Default for LogViewerState {
info_visible: true,
debug_visible: true,
trace_visible: true,
scroll_state: ScrollState::Auto,
}
}
}

#[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,
Expand Down Expand Up @@ -79,9 +93,11 @@ pub fn setup_log_viewer_ui(mut commands: Commands, log_viewer_res: Res<LogViewer
justify_content: JustifyContent::Stretch,
position_type: PositionType::Absolute,
overflow: Overflow::clip(),
border: UiRect::bottom(Val::Px(1.)),
..default()
},
BackgroundColor(Color::srgba(0.15, 0.15, 0.15, 0.75)),
BorderColor(Color::NONE),
))
.with_children(|parent| {
// Title Bar
Expand Down Expand Up @@ -246,25 +262,91 @@ pub fn setup_log_viewer_ui(mut commands: Commands, log_viewer_res: Res<LogViewer
});
});

// Button for scrolling to the bottom
parent
.spawn((
Node {
width: Val::Px(24.),
height: Val::Px(24.),
bottom: Val::Px(5.),
right: Val::Px(5.),
position_type: PositionType::Absolute,
display: Display::None,
margin: UiRect::all(Val::Px(1.)),
padding: UiRect {
left: Val::Px(5.),
right: Val::Px(5.),
top: Val::Px(10.),
bottom: Val::Px(5.),
},
border: UiRect::all(Val::Px(1.)),
..default()
},
ZIndex(1),
Button,
BorderColor(Color::WHITE),
BorderRadius::all(Val::Px(20.)),
BackgroundColor(Color::BLACK.with_alpha(0.75)),
GoDownBtnMarker,
Name::new("go_down_btn"),
))
.observe(|_: Trigger<Pointer<Click>>, 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.
parent
.spawn((
Node {
overflow: Overflow::clip_y(),
align_items: AlignItems::End,
justify_content: JustifyContent::Center,
width: Val::Px(16.),
height: Val::Px(8.),
padding: UiRect::bottom(Val::Px(4.)),
..default()
},
Name::new("icon_container"),
))
.with_children(|parent| {
parent.spawn((
Node {
width: Val::Px(8.),
height: Val::Px(8.),
..default()
},
Transform {
rotation: Quat::from_rotation_z(std::f32::consts::FRAC_PI_4),
..default()
},
BackgroundColor(Color::WHITE),
Name::new("down_arrow"),
));
});
});

// List Container
parent
.spawn((
Node {
height: Val::Percent(100.),
overflow: Overflow {
x: OverflowAxis::Visible,
y: OverflowAxis::Clip,
},
overflow: Overflow::scroll_y(),
..default()
},
Name::new("container"),
ListContainerMarker,
))
.observe(on_drag_scroll)
.with_children(|children| {
children.spawn((
Node {
flex_direction: FlexDirection::Column,
position_type: PositionType::Absolute,
bottom: Val::Px(0.),
..default()
},
PickingBehavior {
should_block_lower: false,
..default()
},
Name::new("list"),
Expand All @@ -273,3 +355,14 @@ pub fn setup_log_viewer_ui(mut commands: Commands, log_viewer_res: Res<LogViewer
});
});
}

fn on_drag_scroll(
drag: Trigger<Pointer<Drag>>,
mut scroll_positions: Query<&mut ScrollPosition, With<ListContainerMarker>>,
mut log_viewer_state: ResMut<LogViewerState>,
) {
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;
}
}
124 changes: 120 additions & 4 deletions src/logging.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
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},
};
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,
metadata: &'static tracing::Metadata<'static>,
timestamp: OffsetDateTime,
}

#[derive(Debug, Event, Clone)]
pub(crate) struct ScrollToBottom;

#[derive(Deref, DerefMut)]
struct LogEventsReceiver(mpsc::Receiver<LogEvent>);

Expand Down Expand Up @@ -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);

Expand All @@ -127,14 +136,18 @@ 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,
(
on_traffic_light_button,
on_auto_open_check,
on_level_filter_chip,
(handle_listcontainer_overflow, handle_scroll_update).chain(),
),
);
}
Expand Down Expand Up @@ -487,6 +500,9 @@ fn receive_logs(
{
commands.trigger(LogViewerVisibility::Show);
}
if log_viewer_res.scroll_state == ScrollState::Auto {
commands.trigger(ScrollToBottom);
}
}
}
}
Expand All @@ -510,12 +526,87 @@ 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<ListContainerMarker>>,
child_comp_node_query: Query<&ComputedNode, With<ListMarker>>,
) {
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<LogViewerState>,
mut border_color_q: Query<&mut BorderColor, With<LogViewerMarker>>,
mut scroll_to_bottom_btn_q: Query<&mut Node, With<GoDownBtnMarker>>,
mut scroll_query: Query<(&ScrollPosition, &ComputedNode, &Children), With<ListContainerMarker>>,
child_comp_node_query: Query<&ComputedNode, With<ListMarker>>,
) {
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;

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 {
ScrollState::Auto
} else {
ScrollState::Manual
};
}
}
}

fn handle_scroll_to_bottom(
_trigger: Trigger<ScrollToBottom>,
mut log_viewer: ResMut<LogViewerState>,
mut scroll_query: Query<(&mut ScrollPosition, &Children), With<ListContainerMarker>>,
computed_node_query: Query<&ComputedNode, With<ListMarker>>,
) {
// 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,
Expand Down Expand Up @@ -594,3 +685,28 @@ fn on_level_filter_chip(
}
}
}

fn handle_scroll_update(
mut mouse_wheel_events: EventReader<MouseWheel>,
hover_map: Res<HoverMap>,
mut scrolled_node_query: Query<&mut ScrollPosition, With<ListContainerMarker>>,
) {
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;
}
}
}
}
}
Loading