Rebase !1447 Chat input color and icon reflect channel message is sent to.

This commit is contained in:
Quellus 2021-02-10 19:42:59 +00:00 committed by Justin Shipsey
parent f2660ef5f9
commit 63952875d9
6 changed files with 104 additions and 39 deletions

View File

@ -23,8 +23,8 @@ use common::{
group, group,
skills::Skill, skills::Skill,
slot::Slot, slot::Slot,
ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, InventoryManip, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip,
InventoryUpdateEvent, LoadoutManip, InventoryManip, InventoryUpdateEvent, LoadoutManip,
}, },
event::{EventBus, LocalEvent}, event::{EventBus, LocalEvent},
grid::Grid, grid::Grid,
@ -124,6 +124,7 @@ pub struct Client {
player_list: HashMap<Uid, PlayerInfo>, player_list: HashMap<Uid, PlayerInfo>,
character_list: CharacterList, character_list: CharacterList,
sites: Vec<SiteInfo>, sites: Vec<SiteInfo>,
pub chat_mode: ChatMode,
recipe_book: RecipeBook, recipe_book: RecipeBook,
available_recipes: HashSet<String>, available_recipes: HashSet<String>,
@ -416,6 +417,7 @@ impl Client {
sites, sites,
recipe_book, recipe_book,
available_recipes: HashSet::default(), available_recipes: HashSet::default(),
chat_mode: ChatMode::default(),
max_group_size, max_group_size,
group_invite: None, group_invite: None,
@ -1346,6 +1348,9 @@ impl Client {
} }
}, },
ServerGeneral::ChatMsg(m) => frontend_events.push(Event::Chat(m)), ServerGeneral::ChatMsg(m) => frontend_events.push(Event::Chat(m)),
ServerGeneral::ChatMode(m) => {
self.chat_mode = m;
},
ServerGeneral::SetPlayerEntity(uid) => { ServerGeneral::SetPlayerEntity(uid) => {
if let Some(entity) = self.state.ecs().entity_from_uid(uid.0) { if let Some(entity) = self.state.ecs().entity_from_uid(uid.0) {
self.entity = entity; self.entity = entity;

View File

@ -110,6 +110,7 @@ pub enum ServerGeneral {
/// A message to go into the client chat box. The client is responsible for /// A message to go into the client chat box. The client is responsible for
/// formatting the message and turning it into a speech bubble. /// formatting the message and turning it into a speech bubble.
ChatMsg(comp::ChatMsg), ChatMsg(comp::ChatMsg),
ChatMode(comp::ChatMode),
SetPlayerEntity(Uid), SetPlayerEntity(Uid),
TimeOfDay(TimeOfDay), TimeOfDay(TimeOfDay),
EntitySync(sync::EntitySyncPackage), EntitySync(sync::EntitySyncPackage),
@ -230,6 +231,7 @@ impl ServerMsg {
// Always possible // Always possible
ServerGeneral::PlayerListUpdate(_) ServerGeneral::PlayerListUpdate(_)
| ServerGeneral::ChatMsg(_) | ServerGeneral::ChatMsg(_)
| ServerGeneral::ChatMode(_)
| ServerGeneral::SetPlayerEntity(_) | ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_) | ServerGeneral::TimeOfDay(_)
| ServerGeneral::EntitySync(_) | ServerGeneral::EntitySync(_)

View File

@ -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 /// A player's current chat mode. These are chat types that can only be sent by
/// the player. /// the player.
#[derive(Clone, Debug)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ChatMode { pub enum ChatMode {
/// Private message to another player (by uuid) /// Private message to another player (by uuid)
Tell(Uid), Tell(Uid),

View File

@ -94,6 +94,7 @@ impl Client {
// Always possible // Always possible
ServerGeneral::PlayerListUpdate(_) ServerGeneral::PlayerListUpdate(_)
| ServerGeneral::ChatMsg(_) | ServerGeneral::ChatMsg(_)
| ServerGeneral::ChatMode(_)
| ServerGeneral::SetPlayerEntity(_) | ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_) | ServerGeneral::TimeOfDay(_)
| ServerGeneral::EntitySync(_) | ServerGeneral::EntitySync(_)
@ -169,6 +170,7 @@ impl Client {
// Always possible // Always possible
ServerGeneral::PlayerListUpdate(_) ServerGeneral::PlayerListUpdate(_)
| ServerGeneral::ChatMsg(_) | ServerGeneral::ChatMsg(_)
| ServerGeneral::ChatMode(_)
| ServerGeneral::SetPlayerEntity(_) | ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_) | ServerGeneral::TimeOfDay(_)
| ServerGeneral::EntitySync(_) | ServerGeneral::EntitySync(_)

View File

@ -1510,6 +1510,7 @@ fn handle_tell(
.insert(client, mode.clone()); .insert(client, mode.clone());
let msg = message_opt.unwrap_or_else(|| format!("{} wants to talk to you.", alias)); 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.state.send_chat(mode.new_message(client_uid, msg));
server.notify_client(client, ServerGeneral::ChatMode(mode));
} else { } else {
server.notify_client( server.notify_client(
client, client,
@ -1551,6 +1552,7 @@ fn handle_faction(
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.new_message(*uid, msg));
} }
} }
server.notify_client(client, ServerGeneral::ChatMode(mode));
} else { } else {
server.notify_client( server.notify_client(
client, client,
@ -1586,6 +1588,7 @@ fn handle_group(
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.new_message(*uid, msg));
} }
} }
server.notify_client(client, ServerGeneral::ChatMode(mode));
} else { } else {
server.notify_client( server.notify_client(
client, client,
@ -1767,6 +1770,7 @@ fn handle_region(
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.new_message(*uid, msg));
} }
} }
server.notify_client(client, ServerGeneral::ChatMode(mode));
} }
fn handle_say( fn handle_say(
@ -1795,6 +1799,7 @@ fn handle_say(
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.new_message(*uid, msg));
} }
} }
server.notify_client(client, ServerGeneral::ChatMode(mode));
} }
fn handle_world( fn handle_world(
@ -1823,6 +1828,7 @@ fn handle_world(
server.state.send_chat(mode.new_message(*uid, msg)); server.state.send_chat(mode.new_message(*uid, msg));
} }
} }
server.notify_client(client, ServerGeneral::ChatMode(mode));
} }
fn handle_join_faction( fn handle_join_faction(
@ -1847,9 +1853,14 @@ fn handle_join_faction(
.get(target) .get(target)
.map(|player| player.alias.clone()) .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 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 let faction_leave = server
.state .state
.ecs() .ecs()
@ -1862,16 +1873,21 @@ fn handle_join_faction(
ChatType::FactionMeta(faction.clone()) ChatType::FactionMeta(faction.clone())
.chat_msg(format!("[{}] joined faction ({})", alias, faction)), .chat_msg(format!("[{}] joined faction ({})", alias, faction)),
); );
faction_leave (faction_leave, mode)
} else { } else {
let mode = comp::ChatMode::default(); let mode = comp::ChatMode::default();
let _ = server.state.ecs().write_storage().insert(client, mode); let _ = server
server .state
.ecs()
.write_storage()
.insert(client, mode.clone());
let faction_leave = server
.state .state
.ecs() .ecs()
.write_storage() .write_storage()
.remove(client) .remove(client)
.map(|comp::Faction(f)| f) .map(|comp::Faction(f)| f);
(faction_leave, mode)
}; };
if let Some(faction) = faction_leave { if let Some(faction) = faction_leave {
server.state.send_chat( server.state.send_chat(
@ -1879,6 +1895,7 @@ fn handle_join_faction(
.chat_msg(format!("[{}] left faction ({})", alias, faction)), .chat_msg(format!("[{}] left faction ({})", alias, faction)),
); );
} }
server.notify_client(client, ServerGeneral::ChatMode(mode));
} else { } else {
server.notify_client( server.notify_client(
client, client,

View File

@ -1,12 +1,12 @@
use super::{ use super::{
img_ids::Imgs, ERROR_COLOR, FACTION_COLOR, GROUP_COLOR, INFO_COLOR, KILL_COLOR, LOOT_COLOR, 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 crate::{i18n::Localization, ui::fonts::Fonts, GlobalState};
use client::{cmd, Client}; use client::{cmd, Client};
use common::comp::{ use common::comp::{
chat::{KillSource, KillType}, chat::{KillSource, KillType},
ChatMsg, ChatType, ChatMode, ChatMsg, ChatType,
}; };
use common_net::msg::validate_chat_msg; use common_net::msg::validate_chat_msg;
use conrod_core::{ use conrod_core::{
@ -27,6 +27,7 @@ widget_ids! {
message_box_bg, message_box_bg,
chat_input, chat_input,
chat_input_bg, chat_input_bg,
chat_input_icon,
chat_arrow, chat_arrow,
chat_icons[], chat_icons[],
} }
@ -36,8 +37,10 @@ const X: f64 = 18.0;*/
const MAX_MESSAGES: usize = 100; 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_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; const CHAT_BOX_HEIGHT: f64 = 174.0;
#[derive(WidgetCommon)] #[derive(WidgetCommon)]
@ -121,9 +124,14 @@ impl<'a> Chat<'a> {
} }
} }
struct InputState {
message: String,
mode: ChatMode,
}
pub struct State { pub struct State {
messages: VecDeque<ChatMsg>, messages: VecDeque<ChatMsg>,
input: String, input: InputState,
ids: Ids, ids: Ids,
history: VecDeque<String>, history: VecDeque<String>,
// Index into the history Vec, history_pos == 0 is history not in use // 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 { fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
State { State {
input: "".to_owned(), input: InputState {
message: "".to_owned(),
mode: ChatMode::default(),
},
messages: VecDeque::new(), messages: VecDeque::new(),
history: VecDeque::new(), history: VecDeque::new(),
history_pos: 0, history_pos: 0,
@ -212,8 +223,8 @@ impl<'a> Widget for Chat<'a> {
false false
} else if let Some(cursor) = state.completion_cursor { } else if let Some(cursor) = state.completion_cursor {
// Cycle through tab completions of the current word // Cycle through tab completions of the current word
if state.input.contains('\t') { if state.input.message.contains('\t') {
state.update(|s| s.input.retain(|c| c != '\t')); state.update(|s| s.input.message.retain(|c| c != '\t'));
//tab_dir + 1 //tab_dir + 1
} }
if !state.completions.is_empty() && (tab_dir != 0 || state.completions_index.is_none()) if !state.completions.is_empty() && (tab_dir != 0 || state.completions_index.is_none())
@ -225,14 +236,15 @@ impl<'a> Widget for Chat<'a> {
% len, % len,
); );
if let Some(replacement) = &s.completions.get(s.completions_index.unwrap()) { 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); force_cursor = cursor_offset_to_index(offset, &completed, &ui, &self.fonts);
s.input = completed; s.input.message = completed;
} }
}); });
} }
false false
} else if let Some(cursor) = state.input.find('\t') { } else if let Some(cursor) = state.input.message.find('\t') {
// Begin tab completion // Begin tab completion
state.update(|s| s.completion_cursor = Some(cursor)); state.update(|s| s.completion_cursor = Some(cursor));
true true
@ -252,11 +264,15 @@ impl<'a> Widget for Chat<'a> {
s.history_pos -= 1; s.history_pos -= 1;
} }
if s.history_pos > 0 { if s.history_pos > 0 {
s.input = s.history.get(s.history_pos - 1).unwrap().to_owned(); s.input.message = s.history.get(s.history_pos - 1).unwrap().to_owned();
force_cursor = force_cursor = cursor_offset_to_index(
cursor_offset_to_index(s.input.len(), &s.input, &ui, &self.fonts); s.input.message.len(),
&s.input.message,
&ui,
&self.fonts,
);
} else { } 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; let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard;
if let Some(input) = &self.force_input { 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 = let input_focused =
@ -273,12 +289,26 @@ impl<'a> Widget for Chat<'a> {
// Only show if it has the keyboard captured. // Only show if it has the keyboard captured.
// Chat input uses a rectangle as its background. // Chat input uses a rectangle as its background.
if input_focused { 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 // Any changes to this TextEdit's width and font size must be reflected in
// `cursor_offset_to_index` below. // `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) .w(CHAT_BOX_INPUT_WIDTH)
.restrict_to_height(false) .restrict_to_height(false)
.color(TEXT_COLOR) .color(color)
.line_spacing(2.0) .line_spacing(2.0)
.font_size(self.fonts.opensans.scale(15)) .font_size(self.fonts.opensans.scale(15))
.font_id(self.fonts.opensans.conrod_id); .font_id(self.fonts.opensans.conrod_id);
@ -298,13 +328,13 @@ impl<'a> Widget for Chat<'a> {
.set(state.ids.chat_input_bg, ui); .set(state.ids.chat_input_bg, ui);
if let Some(str) = text_edit 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) .set(state.ids.chat_input, ui)
{ {
let mut input = str.to_owned(); let mut input = str.to_owned();
input.retain(|c| c != '\n'); input.retain(|c| c != '\n');
if let Ok(()) = validate_chat_msg(&input) { 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); item.set(text.h(y), ui);
let icon_id = state.ids.chat_icons[item.i]; let icon_id = state.ids.chat_icons[item.i];
Image::new(icon) Image::new(icon)
.w_h(16.0, 16.0) .w_h(CHAT_ICON_WIDTH, CHAT_ICON_HEIGHT)
.top_left_with_margins_on(item.widget_id, 2.0, -16.0) .top_left_with_margins_on(item.widget_id, 2.0, -CHAT_ICON_WIDTH)
.parent(state.ids.message_box_bg) .parent(state.ids.message_box_bg)
.set(icon_id, ui); .set(icon_id, ui);
} else { } else {
@ -475,22 +505,19 @@ impl<'a> Widget for Chat<'a> {
// We've started a new tab completion. Populate tab completion suggestions. // We've started a new tab completion. Populate tab completion suggestions.
if request_tab_completions { 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 // If the chat widget is focused, return a focus event to pass the focus
// to the input box. // to the input box.
} else if keyboard_capturer == Some(id) { } else if keyboard_capturer == Some(id) {
Some(Event::Focus(state.ids.chat_input)) Some(Event::Focus(state.ids.chat_input))
} }
// If enter is pressed and the input box is not empty, send the current message. // If enter is pressed and the input box is not empty, send the current message.
else if ui else if ui.widget_input(state.ids.chat_input).presses().key().any(
.widget_input(state.ids.chat_input) |key_press| matches!(key_press.key, Key::Return if !state.input.message.is_empty()),
.presses() ) {
.key() let msg = state.input.message.clone();
.any(|key_press| matches!(key_press.key, Key::Return if !state.input.is_empty()))
{
let msg = state.input.clone();
state.update(|s| { state.update(|s| {
s.input.clear(); s.input.message.clear();
// Update the history // Update the history
// Don't add if this is identical to the last message in the history // Don't add if this is identical to the last message in the history
s.history_pos = 0; 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) 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 /// Get the color and icon for the current line in the chat box
fn render_chat_line(chat_type: &ChatType<String>, imgs: &Imgs) -> (Color, conrod_core::image::Id) { fn render_chat_line(chat_type: &ChatType<String>, imgs: &Imgs) -> (Color, conrod_core::image::Id) {
match chat_type { match chat_type {