From e46a9a2aa9cfab627013c95ec8734d8fe784a11a Mon Sep 17 00:00:00 2001 From: Mitchell Date: Tue, 20 Oct 2026 16:41:16 -0500 Subject: [PATCH] loby ui impl --- src/lobby.rs | 346 ++++++++++++++++++++++++++++++++++++----------- src/main_menu.rs | 17 ++- 2 files changed, 278 insertions(+), 85 deletions(-) diff --git a/src/lobby.rs b/src/lobby.rs index a456aa0..870c8b6 100644 --- a/src/lobby.rs +++ b/src/lobby.rs @@ -4,10 +4,11 @@ use bevy::{ prelude::*, text::{EditableText, EditableTextFilter, TextCursorStyle}, ui::Checked, - ui_widgets::{RadioButton, RadioGroup, ValueChange}, + ui_widgets::{Activate, Button, RadioButton, RadioGroup, ValueChange}, }; use crate::AppState; +use crate::main_menu::InteractionColors; const PANEL_BACKGROUND: Color = Color::srgb(0.4, 0.4, 0.4); const TEXT_COLOR: Color = Color::srgb(0.9, 0.9, 0.9); @@ -23,16 +24,23 @@ const OPTION_SELECTED_PRESSED_BACKGROUND: Color = Color::srgb(0.07, 0.36, 0.44); pub struct LobbyPlugin; -#[derive(Resource, Debug, Clone, Copy, PartialEq, Eq, Reflect)] +#[derive(Resource, Debug, Clone, PartialEq, Eq, Reflect)] #[reflect(Resource)] pub struct LobbyUiState { pub privacy: LobbyPrivacy, + pub port: u16, + pub password: String, + // TODO: replace with actual connection logic + pub session_active: bool, } impl Default for LobbyUiState { fn default() -> Self { Self { privacy: LobbyPrivacy::Public, + port: 5599, + password: String::new(), + session_active: false, } } } @@ -40,14 +48,26 @@ impl Default for LobbyUiState { #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Reflect)] pub enum LobbyPrivacy { #[default] - Public, - FriendsOnly, InviteOnly, + FriendsOnly, + Public, } #[derive(Component, Debug, Clone, Copy, Default, PartialEq, Eq)] struct LobbyPrivacyOption(LobbyPrivacy); +#[derive(Component, Clone, Default)] +struct PortField; + +#[derive(Component, Clone, Default)] +struct PasswordField; + +#[derive(Component, Clone, Default)] +struct ResetPortButton; + +#[derive(Component, Clone, Default)] +struct SessionStatusLabel; + impl Plugin for LobbyPlugin { fn build(&self, app: &mut App) { app.init_resource::(); @@ -58,7 +78,13 @@ impl Plugin for LobbyPlugin { ) .add_systems( Update, - (update_privacy_controls, style_privacy_options).chain(), + ( + update_privacy_controls, + session_visuals, + sync_lobby_fields, + style_privacy_options, + ) + .chain(), ); app.add_observer(on_privacy_changed); } @@ -88,13 +114,12 @@ fn setup_lobby(mut commands: Commands) { fn build_lobby_ui(mut commands: Commands, root_ui: Single>) { let root_ui = *root_ui; - commands.spawn_scene(bsn! { ChildOf(root_ui) Node { + width: percent(50), flex_direction: FlexDirection::Column, - align_self: AlignSelf::FlexEnd, - flex_grow: 1., + align_self: AlignSelf::Stretch, border_radius: BorderRadius::all(px(24)), padding: Val::all(px(24)), } @@ -106,122 +131,283 @@ fn build_lobby_ui(mut commands: Commands, root_ui: Single, mut fields: Query<&mut EditableText, With>| { + if let Ok(mut field) = fields.single_mut() { + field.editor_mut().set_text("5599"); + } + }) + ), + ] + ), + (field_label("Password")), + (password_field()), + ( + Node { + flex_grow: 1., } - EditableTextFilter::new(|c| c == ' ' || c.is_ascii_alphanumeric()) - TextLayout::no_wrap() - TextFont { - font_size: FontSize::Px(66.), + ), + ( + Button + Node { + width: px(600), + height: px(130), + border: px(6), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + margin: UiRect::all(px(4)), } - TextCursorStyle { - color: Color::BLACK - } - TabIndex(0) - BackgroundColor(TEXT_INPUT_BACKGROUND) + BorderColor::from(Color::BLACK) + InteractionColors::GOOD + Interaction + on(|_event: On, mut lobby: ResMut| { + info!("Session clicked!"); + lobby.session_active = !lobby.session_active; + }) + Children [( + Text("Start Session") + SessionStatusLabel + TextFont { + font_size: px(66.), + } + TextColor(TEXT_COLOR) + TextShadow + )] ), ] }); + commands.spawn_scene(bsn! { ChildOf(root_ui) Node { + width: percent(50), flex_direction: FlexDirection::Column, - align_self: AlignSelf::FlexEnd, - flex_grow: 1., + align_self: AlignSelf::Stretch, border_radius: BorderRadius::all(px(24)), padding: Val::all(px(24)), } BackgroundColor(PANEL_BACKGROUND) Children [ ( - Text("Lobby") + Text("Connected Players") Node { align_self: AlignSelf::Center, } TextFont { - font_size: px(100.0), + font_size: px(76.), } TextColor(TEXT_COLOR) TextShadow ), - ( - Text("Save Name") - Node { - margin: UiRect::axes(px(12), px(0)), - } - TextFont { - // font: FontSourceTemplate::Handle("fonts/FiraSans-Bold.ttf"), - font_size: px(66.0), - } - TextColor(TEXT_COLOR) - TextShadow - ), - ( - Node { - border: Val::all(px(6)), - padding: Val::all(px(12)), - border_radius: BorderRadius::all(px(12)), - } - BorderColor::from(SLATE_900) - EditableText { - cursor_width: 0.05, - max_characters: Option::Some(14), - } - EditableTextFilter::new(|c| c == ' ' || c.is_ascii_alphanumeric()) - TextLayout::no_wrap() - TextFont { - font_size: FontSize::Px(66.), - } - TextCursorStyle { - color: Color::BLACK - } - TabIndex(0) - BackgroundColor(TEXT_INPUT_BACKGROUND) - ), + player_row("Joe (you)"), + player_row("Alex"), + player_row("Sam"), + player_row("Taylor"), ] }); } +fn field_label(label: &'static str) -> impl Scene { + bsn! { + Text(label) + Node { + margin: UiRect::top(px(18)), + } + TextFont { + font_size: px(38.), + } + TextColor(TEXT_COLOR) + } +} + +fn port_field() -> impl Scene { + bsn! { + PortField + Node { + width: percent(100), + border: Val::all(px(6)), + padding: Val::all(px(12)), + border_radius: BorderRadius::all(px(12)), + } + BorderColor::from(SLATE_900) + EditableText::new("5599") + EditableText { + cursor_width: 0.05, + max_characters: Option::Some(5), + } + EditableTextFilter::new(|c| c.is_ascii_digit()) + TextLayout::no_wrap() + TextFont { + font_size: FontSize::Px(66.), + } + TextCursorStyle { + color: Color::BLACK, + } + TabIndex(0) + BackgroundColor(TEXT_INPUT_BACKGROUND) + } +} +fn password_field() -> impl Scene { + bsn! { + PasswordField + Node { + width: percent(100), + border: Val::all(px(6)), + padding: Val::all(px(12)), + border_radius: BorderRadius::all(px(12)), + } + BorderColor::from(SLATE_900) + EditableText { + cursor_width: 0.05, + max_characters: Option::Some(32), + } + EditableTextFilter::new(|c| c == ' ' || c.is_ascii_alphanumeric() || c == '_' || c == '-') + TextLayout::no_wrap() + TextFont { + font_size: FontSize::Px(66.), + } + TextCursorStyle { + color: Color::BLACK, + } + TabIndex(1) + BackgroundColor(TEXT_INPUT_BACKGROUND) + } +} + +fn player_row(name: &'static str) -> impl Scene { + bsn! { + Node { + width: percent(100), + height: px(130), + margin: UiRect::top(px(14)), + padding: Val::all(px(16)), + align_items: AlignItems::Center, + column_gap: px(24), + border_radius: BorderRadius::all(px(12)), + } + BackgroundColor(OPTION_BACKGROUND) + Children [ + ( + Node { + width: px(96), + height: px(96), + border_radius: BorderRadius::MAX, + } + BackgroundColor(OPTION_SELECTED_BACKGROUND) + ), + ( + Text(name) + TextFont { + font_size: px(50.), + } + TextColor(TEXT_COLOR) + TextShadow + ), + ] + } +} + +fn sync_lobby_fields( + port_field: Single<&EditableText, With>, + password_field: Single<&EditableText, With>, + mut lobby: ResMut, +) { + if let Ok(port) = port_field.value().to_string().parse::() { + lobby.port = port; + } + lobby.password = password_field.value().to_string(); +} + +fn session_visuals( + lobby_ui: Res, + q_session_control_text: Option>>, +) { + if !lobby_ui.is_changed() { + return; + } + + let Some(mut text) = q_session_control_text else { + warn!("Session Control Text not found!"); + return; + }; + + let new_text = match lobby_ui.session_active { + true => "Stop Session", + false => "Start Session", + }; + **text = new_text.into(); +} + fn privacy_option( label: &'static str, privacy: LobbyPrivacy, @@ -236,7 +422,7 @@ fn privacy_option( flex_grow: 1., justify_content: JustifyContent::Center, align_items: AlignItems::Center, - border: UiRect::all(px(3)), + border: UiRect::all(px(4)), border_radius, } BorderColor::all(OPTION_BORDER_COLOR) @@ -257,6 +443,10 @@ fn update_privacy_controls( mut commands: Commands, options: Query<(Entity, &LobbyPrivacyOption, &Interaction, Has), With>, ) { + if !lobby_ui.is_changed() { + return; + } + for (entity, option, interaction, checked) in &options { let should_be_checked = option.0 == lobby_ui.privacy; if should_be_checked && !checked { diff --git a/src/main_menu.rs b/src/main_menu.rs index 95749ec..940d676 100644 --- a/src/main_menu.rs +++ b/src/main_menu.rs @@ -32,7 +32,7 @@ struct Selected; /// Defines colors that a UI object should turn when interactions occur. #[derive(Component, Clone, PartialEq, Default, Debug)] -struct InteractionColors { +pub(crate) struct InteractionColors { default: Color, hovered: Color, pressed: Color, @@ -137,7 +137,7 @@ impl MenuLayer { menu_layer(layer) ChildOf(menu_root) Children [ - {entries} + {entries}, ( menu_button("New Game", Some(MenuLayer::NewGame)) InteractionColors::GOOD @@ -163,7 +163,7 @@ impl MenuLayer { full_menu_layer(layer) ChildOf(menu_root) Children [ - menu_button("Game 1", None), + menu_button("Game 1", None) ] }), MenuLayer::DirectConnect => Box::new(bsn! { @@ -320,7 +320,7 @@ fn new_game_menu() -> impl SceneList { font_size: FontSize::Px(66.), } TextCursorStyle { - color: Color::BLACK + color: Color::BLACK, } TabIndex(0) BackgroundColor(Color::srgb(0.48, 0.84, 0.87)) @@ -394,7 +394,7 @@ fn game_info_menu(name: impl Into) -> impl SceneList { font_size: FontSize::Px(66.), } TextCursorStyle { - color: Color::BLACK + color: Color::BLACK, } TabIndex(0) BackgroundColor(Color::srgb(0.48, 0.84, 0.87)) @@ -434,7 +434,7 @@ fn game_info_menu(name: impl Into) -> impl SceneList { // TODO: refresh listing }) - ) + ), ] ), ] @@ -504,11 +504,14 @@ fn menu_button(label: impl Into, next_layer: Option) -> Box, mut commands: Commands, - mut menu: ResMut, + menu: Option>, q_next_layer: Query<(&NextLayer, &ChildOf)>, q_layer: Query<(&Layer, &Children)>, q_selected: Query<(Entity, Has)>, ) { + let Some(mut menu) = menu else { + return; + }; if let Ok((next_layer, parent)) = q_next_layer.get(event.entity) { let Ok((layer, layer_entries)) = q_layer.get(parent.parent()) else { warn!(