From 63952875d98f4205daeb761ee87c3604ed6bab15 Mon Sep 17 00:00:00 2001 From: Quellus Date: Wed, 10 Feb 2021 19:42:59 +0000 Subject: [PATCH] Rebase !1447 Chat input color and icon reflect channel message is sent to. --- client/src/lib.rs | 9 +++- common/net/src/msg/server.rs | 2 + common/src/comp/chat.rs | 2 +- server/src/client.rs | 2 + server/src/cmd.rs | 29 ++++++++--- voxygen/src/hud/chat.rs | 99 +++++++++++++++++++++++++----------- 6 files changed, 104 insertions(+), 39 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index c18a1a1bcd..043f2536fc 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -23,8 +23,8 @@ use common::{ group, skills::Skill, slot::Slot, - ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, InventoryManip, - InventoryUpdateEvent, LoadoutManip, + ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, + InventoryManip, InventoryUpdateEvent, LoadoutManip, }, event::{EventBus, LocalEvent}, grid::Grid, @@ -124,6 +124,7 @@ pub struct Client { player_list: HashMap, character_list: CharacterList, sites: Vec, + pub chat_mode: ChatMode, recipe_book: RecipeBook, available_recipes: HashSet, @@ -416,6 +417,7 @@ impl Client { sites, recipe_book, available_recipes: HashSet::default(), + chat_mode: ChatMode::default(), max_group_size, group_invite: None, @@ -1346,6 +1348,9 @@ impl Client { } }, ServerGeneral::ChatMsg(m) => frontend_events.push(Event::Chat(m)), + ServerGeneral::ChatMode(m) => { + self.chat_mode = m; + }, ServerGeneral::SetPlayerEntity(uid) => { if let Some(entity) = self.state.ecs().entity_from_uid(uid.0) { self.entity = entity; diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index e254cfcd64..bf8723da77 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -110,6 +110,7 @@ pub enum ServerGeneral { /// A message to go into the client chat box. The client is responsible for /// formatting the message and turning it into a speech bubble. ChatMsg(comp::ChatMsg), + ChatMode(comp::ChatMode), SetPlayerEntity(Uid), TimeOfDay(TimeOfDay), EntitySync(sync::EntitySyncPackage), @@ -230,6 +231,7 @@ impl ServerMsg { // Always possible ServerGeneral::PlayerListUpdate(_) | ServerGeneral::ChatMsg(_) + | ServerGeneral::ChatMode(_) | ServerGeneral::SetPlayerEntity(_) | ServerGeneral::TimeOfDay(_) | ServerGeneral::EntitySync(_) diff --git a/common/src/comp/chat.rs b/common/src/comp/chat.rs index eaa06941fb..b0c0470760 100644 --- a/common/src/comp/chat.rs +++ b/common/src/comp/chat.rs @@ -9,7 +9,7 @@ use std::time::{Duration, Instant}; /// A player's current chat mode. These are chat types that can only be sent by /// the player. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum ChatMode { /// Private message to another player (by uuid) Tell(Uid), diff --git a/server/src/client.rs b/server/src/client.rs index 068ae649f6..27a488c3b1 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -94,6 +94,7 @@ impl Client { // Always possible ServerGeneral::PlayerListUpdate(_) | ServerGeneral::ChatMsg(_) + | ServerGeneral::ChatMode(_) | ServerGeneral::SetPlayerEntity(_) | ServerGeneral::TimeOfDay(_) | ServerGeneral::EntitySync(_) @@ -169,6 +170,7 @@ impl Client { // Always possible ServerGeneral::PlayerListUpdate(_) | ServerGeneral::ChatMsg(_) + | ServerGeneral::ChatMode(_) | ServerGeneral::SetPlayerEntity(_) | ServerGeneral::TimeOfDay(_) | ServerGeneral::EntitySync(_) diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 3bd8ead2a9..b5321fc90d 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1510,6 +1510,7 @@ fn handle_tell( .insert(client, mode.clone()); let msg = message_opt.unwrap_or_else(|| format!("{} wants to talk to you.", alias)); server.state.send_chat(mode.new_message(client_uid, msg)); + server.notify_client(client, ServerGeneral::ChatMode(mode)); } else { server.notify_client( client, @@ -1551,6 +1552,7 @@ fn handle_faction( server.state.send_chat(mode.new_message(*uid, msg)); } } + server.notify_client(client, ServerGeneral::ChatMode(mode)); } else { server.notify_client( client, @@ -1586,6 +1588,7 @@ fn handle_group( server.state.send_chat(mode.new_message(*uid, msg)); } } + server.notify_client(client, ServerGeneral::ChatMode(mode)); } else { server.notify_client( client, @@ -1767,6 +1770,7 @@ fn handle_region( server.state.send_chat(mode.new_message(*uid, msg)); } } + server.notify_client(client, ServerGeneral::ChatMode(mode)); } fn handle_say( @@ -1795,6 +1799,7 @@ fn handle_say( server.state.send_chat(mode.new_message(*uid, msg)); } } + server.notify_client(client, ServerGeneral::ChatMode(mode)); } fn handle_world( @@ -1823,6 +1828,7 @@ fn handle_world( server.state.send_chat(mode.new_message(*uid, msg)); } } + server.notify_client(client, ServerGeneral::ChatMode(mode)); } fn handle_join_faction( @@ -1847,9 +1853,14 @@ fn handle_join_faction( .get(target) .map(|player| player.alias.clone()) { - let faction_leave = if let Ok(faction) = scan_fmt!(&args, &action.arg_fmt(), String) { + let (faction_leave, mode) = if let Ok(faction) = scan_fmt!(&args, &action.arg_fmt(), String) + { let mode = comp::ChatMode::Faction(faction.clone()); - let _ = server.state.ecs().write_storage().insert(client, mode); + let _ = server + .state + .ecs() + .write_storage() + .insert(client, mode.clone()); let faction_leave = server .state .ecs() @@ -1862,16 +1873,21 @@ fn handle_join_faction( ChatType::FactionMeta(faction.clone()) .chat_msg(format!("[{}] joined faction ({})", alias, faction)), ); - faction_leave + (faction_leave, mode) } else { let mode = comp::ChatMode::default(); - let _ = server.state.ecs().write_storage().insert(client, mode); - server + let _ = server + .state + .ecs() + .write_storage() + .insert(client, mode.clone()); + let faction_leave = server .state .ecs() .write_storage() .remove(client) - .map(|comp::Faction(f)| f) + .map(|comp::Faction(f)| f); + (faction_leave, mode) }; if let Some(faction) = faction_leave { server.state.send_chat( @@ -1879,6 +1895,7 @@ fn handle_join_faction( .chat_msg(format!("[{}] left faction ({})", alias, faction)), ); } + server.notify_client(client, ServerGeneral::ChatMode(mode)); } else { server.notify_client( client, diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index cb281428ff..b2dc69c780 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -1,12 +1,12 @@ use super::{ img_ids::Imgs, ERROR_COLOR, FACTION_COLOR, GROUP_COLOR, INFO_COLOR, KILL_COLOR, LOOT_COLOR, - OFFLINE_COLOR, ONLINE_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR, WORLD_COLOR, + OFFLINE_COLOR, ONLINE_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, WORLD_COLOR, }; use crate::{i18n::Localization, ui::fonts::Fonts, GlobalState}; use client::{cmd, Client}; use common::comp::{ chat::{KillSource, KillType}, - ChatMsg, ChatType, + ChatMode, ChatMsg, ChatType, }; use common_net::msg::validate_chat_msg; use conrod_core::{ @@ -27,6 +27,7 @@ widget_ids! { message_box_bg, chat_input, chat_input_bg, + chat_input_icon, chat_arrow, chat_icons[], } @@ -36,8 +37,10 @@ const X: f64 = 18.0;*/ const MAX_MESSAGES: usize = 100; +const CHAT_ICON_WIDTH: f64 = 16.0; +const CHAT_ICON_HEIGHT: f64 = 16.0; const CHAT_BOX_WIDTH: f64 = 470.0; -const CHAT_BOX_INPUT_WIDTH: f64 = 460.0; +const CHAT_BOX_INPUT_WIDTH: f64 = 460.0 - CHAT_ICON_WIDTH - 1.0; const CHAT_BOX_HEIGHT: f64 = 174.0; #[derive(WidgetCommon)] @@ -121,9 +124,14 @@ impl<'a> Chat<'a> { } } +struct InputState { + message: String, + mode: ChatMode, +} + pub struct State { messages: VecDeque, - input: String, + input: InputState, ids: Ids, history: VecDeque, // Index into the history Vec, history_pos == 0 is history not in use @@ -149,7 +157,10 @@ impl<'a> Widget for Chat<'a> { fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { State { - input: "".to_owned(), + input: InputState { + message: "".to_owned(), + mode: ChatMode::default(), + }, messages: VecDeque::new(), history: VecDeque::new(), history_pos: 0, @@ -212,8 +223,8 @@ impl<'a> Widget for Chat<'a> { false } else if let Some(cursor) = state.completion_cursor { // Cycle through tab completions of the current word - if state.input.contains('\t') { - state.update(|s| s.input.retain(|c| c != '\t')); + if state.input.message.contains('\t') { + state.update(|s| s.input.message.retain(|c| c != '\t')); //tab_dir + 1 } if !state.completions.is_empty() && (tab_dir != 0 || state.completions_index.is_none()) @@ -225,14 +236,15 @@ impl<'a> Widget for Chat<'a> { % len, ); if let Some(replacement) = &s.completions.get(s.completions_index.unwrap()) { - let (completed, offset) = do_tab_completion(cursor, &s.input, replacement); + let (completed, offset) = + do_tab_completion(cursor, &s.input.message, replacement); force_cursor = cursor_offset_to_index(offset, &completed, &ui, &self.fonts); - s.input = completed; + s.input.message = completed; } }); } false - } else if let Some(cursor) = state.input.find('\t') { + } else if let Some(cursor) = state.input.message.find('\t') { // Begin tab completion state.update(|s| s.completion_cursor = Some(cursor)); true @@ -252,11 +264,15 @@ impl<'a> Widget for Chat<'a> { s.history_pos -= 1; } if s.history_pos > 0 { - s.input = s.history.get(s.history_pos - 1).unwrap().to_owned(); - force_cursor = - cursor_offset_to_index(s.input.len(), &s.input, &ui, &self.fonts); + s.input.message = s.history.get(s.history_pos - 1).unwrap().to_owned(); + force_cursor = cursor_offset_to_index( + s.input.message.len(), + &s.input.message, + &ui, + &self.fonts, + ); } else { - s.input.clear(); + s.input.message.clear(); } }); } @@ -264,7 +280,7 @@ impl<'a> Widget for Chat<'a> { let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard; if let Some(input) = &self.force_input { - state.update(|s| s.input = input.to_string()); + state.update(|s| s.input.message = input.to_string()); } let input_focused = @@ -273,12 +289,26 @@ impl<'a> Widget for Chat<'a> { // Only show if it has the keyboard captured. // Chat input uses a rectangle as its background. if input_focused { + // Shallow comparison of ChatMode. + let discrim = |x| std::mem::discriminant(x); + if discrim(&state.input.mode) != discrim(&self.client.chat_mode) { + state.update(|s| { + s.input.mode = self.client.chat_mode.clone(); + }); + } + + let (color, icon) = render_chat_mode(&state.input.mode, &self.imgs); + Image::new(icon) + .w_h(CHAT_ICON_WIDTH, CHAT_ICON_HEIGHT) + .top_left_with_margin_on(state.ids.chat_input_bg, 2.0) + .set(state.ids.chat_input_icon, ui); + // Any changes to this TextEdit's width and font size must be reflected in // `cursor_offset_to_index` below. - let mut text_edit = TextEdit::new(&state.input) + let mut text_edit = TextEdit::new(&state.input.message) .w(CHAT_BOX_INPUT_WIDTH) .restrict_to_height(false) - .color(TEXT_COLOR) + .color(color) .line_spacing(2.0) .font_size(self.fonts.opensans.scale(15)) .font_id(self.fonts.opensans.conrod_id); @@ -298,13 +328,13 @@ impl<'a> Widget for Chat<'a> { .set(state.ids.chat_input_bg, ui); if let Some(str) = text_edit - .top_left_with_margins_on(state.ids.chat_input_bg, 1.0, 1.0) + .right_from(state.ids.chat_input_icon, 1.0) .set(state.ids.chat_input, ui) { let mut input = str.to_owned(); input.retain(|c| c != '\n'); if let Ok(()) = validate_chat_msg(&input) { - state.update(|s| s.input = input); + state.update(|s| s.input.message = input); } } } @@ -441,8 +471,8 @@ impl<'a> Widget for Chat<'a> { item.set(text.h(y), ui); let icon_id = state.ids.chat_icons[item.i]; Image::new(icon) - .w_h(16.0, 16.0) - .top_left_with_margins_on(item.widget_id, 2.0, -16.0) + .w_h(CHAT_ICON_WIDTH, CHAT_ICON_HEIGHT) + .top_left_with_margins_on(item.widget_id, 2.0, -CHAT_ICON_WIDTH) .parent(state.ids.message_box_bg) .set(icon_id, ui); } else { @@ -475,22 +505,19 @@ impl<'a> Widget for Chat<'a> { // We've started a new tab completion. Populate tab completion suggestions. if request_tab_completions { - Some(Event::TabCompletionStart(state.input.to_string())) + Some(Event::TabCompletionStart(state.input.message.to_string())) // If the chat widget is focused, return a focus event to pass the focus // to the input box. } else if keyboard_capturer == Some(id) { Some(Event::Focus(state.ids.chat_input)) } // If enter is pressed and the input box is not empty, send the current message. - else if ui - .widget_input(state.ids.chat_input) - .presses() - .key() - .any(|key_press| matches!(key_press.key, Key::Return if !state.input.is_empty())) - { - let msg = state.input.clone(); + else if ui.widget_input(state.ids.chat_input).presses().key().any( + |key_press| matches!(key_press.key, Key::Return if !state.input.message.is_empty()), + ) { + let msg = state.input.message.clone(); state.update(|s| { - s.input.clear(); + s.input.message.clear(); // Update the history // Don't add if this is identical to the last message in the history s.history_pos = 0; @@ -557,6 +584,18 @@ fn cursor_offset_to_index(offset: usize, text: &str, ui: &Ui, fonts: &Fonts) -> cursor::index_before_char(infos, offset) } +/// Get the color and icon for a client's ChatMode. +fn render_chat_mode(chat_mode: &ChatMode, imgs: &Imgs) -> (Color, conrod_core::image::Id) { + match chat_mode { + ChatMode::World => (WORLD_COLOR, imgs.chat_world_small), + ChatMode::Say => (SAY_COLOR, imgs.chat_say_small), + ChatMode::Region => (REGION_COLOR, imgs.chat_region_small), + ChatMode::Faction(_) => (FACTION_COLOR, imgs.chat_faction_small), + ChatMode::Group(_) => (GROUP_COLOR, imgs.chat_group_small), + ChatMode::Tell(_) => (TELL_COLOR, imgs.chat_tell_small), + } +} + /// Get the color and icon for the current line in the chat box fn render_chat_line(chat_type: &ChatType, imgs: &Imgs) -> (Color, conrod_core::image::Id) { match chat_type {