/* * This file is part of laurelin_client * Copyright (C) 2023 Jonni Liljamo * * Licensed under GPL-3.0-only. * See LICENSE for licensing information. */ use bevy::{prelude::*, input::mouse::{MouseScrollUnit, MouseWheel}, a11y::{AccessibilityNode, accesskit::NodeBuilder}}; use crate::{AppState, plugins::GameData, game_status::LogSection}; pub struct LogPlugin; impl Plugin for LogPlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() .add_system(setup_log.in_schedule(OnEnter(AppState::InGame))) .add_system(update_log_button) .add_system(toggle_log.run_if(on_event::())) .add_system(update_log.run_if(on_event::())) .add_system(log_mouse_scroll); } } struct ToggleLogEvent; pub struct UpdateLogEvent; const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15); const HOVERED_BUTTON: Color = Color::rgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::rgb(0.35, 0.75, 0.35); #[derive(Component)] struct LogButton; #[derive(Component)] struct LogListParent; #[derive(Component)] struct LogList; #[derive(Component)] struct LogListEntry; fn setup_log(mut commands: Commands, asset_server: Res) { let font = asset_server.load("fonts/FiraMono-Bold.ttf"); // log toggle button commands .spawn(NodeBundle { style: Style { position_type: PositionType::Absolute, position: UiRect { top: Val::Px(20.), right: Val::Px(20.), ..Default::default() }, ..Default::default() }, ..Default::default() }) .with_children(|parent| { parent .spawn(( ButtonBundle { style: Style { size: Size::new(Val::Px(75.), Val::Px(35.)), // center child text justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..Default::default() }, background_color: NORMAL_BUTTON.into(), ..Default::default() }, LogButton, )) .with_children(|parent| { parent.spawn(( TextBundle::from_section( "Log", TextStyle { font: font.clone(), font_size: 20., color: Color::WHITE, }, ), )); }); }); // log window commands .spawn(( NodeBundle { style: Style { flex_direction: FlexDirection::Column, justify_content: JustifyContent::Center, align_items: AlignItems::Center, size: Size { width: Val::Px(475.), height: Val::Percent(75.), }, position_type: PositionType::Absolute, position: UiRect { bottom: Val::Percent(12.5), right: Val::Px(100.), ..Default::default() }, ..Default::default() }, background_color: Color::rgb(0.15, 0.15, 0.15).into(), visibility: Visibility::Hidden, ..Default::default() }, LogListParent, )) .with_children(|parent| { parent .spawn(NodeBundle { style: Style { flex_direction: FlexDirection::Column, //align_self: AlignSelf::Stretch, size: Size { width: Val::Px(450.), height: Val::Percent(95.), }, overflow: Overflow::Hidden, ..Default::default() }, ..Default::default() }) .with_children(|parent| { parent .spawn(( NodeBundle { style: Style { flex_direction: FlexDirection::Column, max_size: Size::UNDEFINED, ..Default::default() }, ..Default::default() }, AccessibilityNode(NodeBuilder::new(bevy::a11y::accesskit::Role::List)), ScrollingList::default(), LogList, )) .with_children(|parent| { parent.spawn(( TextBundle::from_section( format!("Empty"), TextStyle { font: font.clone(), font_size: 20., color: Color::WHITE, }, ), AccessibilityNode(NodeBuilder::new(bevy::a11y::accesskit::Role::ListItem)), LogListEntry, )); }); }); }); } fn update_log_button( mut interaction_query: Query< (&Interaction, &mut BackgroundColor), (Changed, With), >, mut tl_ev_w: EventWriter, ) { for (interaction, mut color) in &mut interaction_query { match interaction { Interaction::Clicked => { *color = PRESSED_BUTTON.into(); tl_ev_w.send(ToggleLogEvent); } Interaction::Hovered => { *color = HOVERED_BUTTON.into(); } Interaction::None => { *color = NORMAL_BUTTON.into(); } } } } fn toggle_log( mut log_parent_query: Query<&mut Visibility, With>, ) { for mut visibility in &mut log_parent_query { if *visibility == Visibility::Hidden { *visibility = Visibility::Inherited; } else { *visibility = Visibility::Hidden; } } } fn update_log( mut commands: Commands, asset_server: Res, log_query: Query>, log_entries_query: Query>, game_data: Res, ) { for entity in &log_entries_query { commands.entity(entity).despawn_recursive(); } for entity in &log_query { let Some(status) = game_data.clone().game_status else { return; }; let font = asset_server.load("fonts/FiraMono-Regular.ttf"); let font_bold = asset_server.load("fonts/FiraMono-Bold.ttf"); let style = TextStyle { font: font.clone(), font_size: 20., color: Color::WHITE, }; let index_spaces = status.actions.len().to_string().len(); for (i, _action) in status.actions.iter().enumerate() { let spaces = " ".repeat(index_spaces - i.to_string().len()); let index_text = TextSection { value: format!("{}{}. ", spaces, i), style: TextStyle { font: font_bold.clone(), ..style }, }; let mut sections = vec![index_text]; let log = status.log.get(i).unwrap(); for section in &log.sections { match section { LogSection::Normal(value) => { sections.push(TextSection { value: value.to_string(), style: style.clone(), }); } LogSection::Bold(value) => { sections.push(TextSection { value: value.to_string(), style: TextStyle { font: font_bold.clone(), ..style.clone() }, }); } } } commands.spawn(( TextBundle::from_sections(sections) .with_style(Style { max_size: Size { width: Val::Px(450.), height: Val::Undefined, }, ..Default::default() }), AccessibilityNode(NodeBuilder::new(bevy::a11y::accesskit::Role::ListItem)), LogListEntry, )) .set_parent(entity); } } } #[derive(Component, Default)] struct ScrollingList { position: f32, } fn log_mouse_scroll( mut mouse_wheel_events: EventReader, mut query_list: Query<(&mut ScrollingList, &mut Style, &Parent, &Node)>, query_node: Query<&Node>, ) { for mouse_wheel_event in mouse_wheel_events.iter() { for (mut scrolling_list, mut style, parent, list_node) in &mut query_list { let items_height = list_node.size().y; let container_height = query_node.get(parent.get()).unwrap().size().y; let max_scroll = (items_height - container_height).max(0.); let dy = match mouse_wheel_event.unit { MouseScrollUnit::Line => mouse_wheel_event.y * 20., MouseScrollUnit::Pixel => mouse_wheel_event.y, }; scrolling_list.position += dy; scrolling_list.position = scrolling_list.position.clamp(-max_scroll, 0.); style.position.top = Val::Px(scrolling_list.position); } } }