1use super::{
2 Show, TEXT_COLOR, TEXT_COLOR_3, UI_HIGHLIGHT_0, UI_MAIN,
3 img_ids::{Imgs, ImgsRot},
4};
5use crate::ui::{ImageFrame, Tooltip, TooltipManager, Tooltipable, fonts::Fonts};
6use chumsky::chain::Chain;
7use client::{self, Client};
8use common::{comp::group, resources::BattleMode, uid::Uid};
9use conrod_core::{
10 Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, color,
11 widget::{self, Button, Image, Rectangle, Scrollbar, Text, TextEdit},
12 widget_ids,
13};
14use i18n::Localization;
15use itertools::Itertools;
16use std::time::Instant;
17
18widget_ids! {
19 pub struct Ids {
20 frame,
21 close,
22 title_align,
23 title,
24 bg,
25 icon,
26 scrollbar,
27 online_align,
28 player_names[],
29 player_pvp_icons[],
30 player_rows[],
31 player_mod_badges[],
32 online_txt,
33 online_no,
34 invite_button,
35 player_search_icon,
36 player_search_input,
37 player_search_input_bg,
38 player_search_input_overlay,
39 pvp_button_on,
40 pvp_button_off,
41 }
42}
43
44pub struct State {
45 ids: Ids,
46 selected_uid: Option<(Uid, Instant)>,
49}
50
51#[derive(WidgetCommon)]
52pub struct Social<'a> {
53 show: &'a Show,
54 client: &'a Client,
55 imgs: &'a Imgs,
56 fonts: &'a Fonts,
57 localized_strings: &'a Localization,
58 selected_entity: Option<(specs::Entity, Instant)>,
59 rot_imgs: &'a ImgsRot,
60 tooltip_manager: &'a mut TooltipManager,
61
62 #[conrod(common_builder)]
63 common: widget::CommonBuilder,
64}
65
66impl<'a> Social<'a> {
67 pub fn new(
68 show: &'a Show,
69 client: &'a Client,
70 imgs: &'a Imgs,
71 fonts: &'a Fonts,
72 localized_strings: &'a Localization,
73 selected_entity: Option<(specs::Entity, Instant)>,
74 rot_imgs: &'a ImgsRot,
75 tooltip_manager: &'a mut TooltipManager,
76 ) -> Self {
77 Self {
78 show,
79 client,
80 imgs,
81 rot_imgs,
82 fonts,
83 localized_strings,
84 tooltip_manager,
85 selected_entity,
86 common: widget::CommonBuilder::default(),
87 }
88 }
89}
90
91pub enum Event {
92 Close,
93 Invite(Uid),
94 Focus(widget::Id),
95 SearchPlayers(Option<String>),
96 SetBattleMode(BattleMode),
97}
98
99impl Widget for Social<'_> {
100 type Event = Vec<Event>;
101 type State = State;
102 type Style = ();
103
104 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
105 Self::State {
106 ids: Ids::new(id_gen),
107 selected_uid: None,
108 }
109 }
110
111 fn style(&self) -> Self::Style {}
112
113 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
114 common_base::prof_span!("Social::update");
115 let battle_mode = self.client.get_battle_mode();
116 let widget::UpdateArgs { state, ui, .. } = args;
117 let mut events = Vec::new();
118 let button_tooltip = Tooltip::new({
119 let edge = &self.rot_imgs.tt_side;
122 let corner = &self.rot_imgs.tt_corner;
123 ImageFrame::new(
124 [edge.cw180, edge.none, edge.cw270, edge.cw90],
125 [corner.none, corner.cw270, corner.cw90, corner.cw180],
126 Color::Rgba(0.08, 0.07, 0.04, 1.0),
127 5.0,
128 )
129 })
130 .title_font_size(self.fonts.cyri.scale(15))
131 .parent(ui.window)
132 .desc_font_size(self.fonts.cyri.scale(12))
133 .font_id(self.fonts.cyri.conrod_id)
134 .desc_text_color(TEXT_COLOR);
135
136 Image::new(self.imgs.social_bg_on)
138 .bottom_left_with_margins_on(ui.window, 308.0, 25.0)
139 .color(Some(UI_MAIN))
140 .w_h(310.0, 460.0)
141 .set(state.ids.bg, ui);
142 Image::new(self.imgs.social_frame_on)
144 .middle_of(state.ids.bg)
145 .color(Some(UI_HIGHLIGHT_0))
146 .w_h(310.0, 460.0)
147 .set(state.ids.frame, ui);
148
149 Image::new(self.imgs.social)
151 .w_h(30.0, 30.0)
152 .top_left_with_margins_on(state.ids.frame, 6.0, 6.0)
153 .set(state.ids.icon, ui);
154 if Button::image(self.imgs.close_button)
156 .w_h(24.0, 25.0)
157 .hover_image(self.imgs.close_button_hover)
158 .press_image(self.imgs.close_button_press)
159 .top_right_with_margins_on(state.ids.frame, 0.0, 0.0)
160 .set(state.ids.close, ui)
161 .was_clicked()
162 {
163 events.push(Event::Close);
164 }
165
166 Rectangle::fill_with([212.0, 42.0], color::TRANSPARENT)
168 .top_left_with_margins_on(state.ids.frame, 2.0, 44.0)
169 .set(state.ids.title_align, ui);
170 Text::new(&self.localized_strings.get_msg("hud-social"))
171 .middle_of(state.ids.title_align)
172 .font_id(self.fonts.cyri.conrod_id)
173 .font_size(self.fonts.cyri.scale(20))
174 .color(TEXT_COLOR)
175 .set(state.ids.title, ui);
176
177 let players = self
178 .client
179 .player_list()
180 .iter()
181 .filter(|(_, p)| p.is_online);
182 let player_count = players.clone().count();
183
184 Rectangle::fill_with([310.0, 346.0], color::TRANSPARENT)
186 .mid_top_with_margin_on(state.ids.frame, 74.0)
187 .scroll_kids_vertically()
188 .set(state.ids.online_align, ui);
189 Scrollbar::y_axis(state.ids.online_align)
190 .thickness(4.0)
191 .color(Color::Rgba(0.79, 1.09, 1.09, 0.0))
192 .set(state.ids.scrollbar, ui);
193
194 Text::new(&self.localized_strings.get_msg("hud-social-online"))
196 .bottom_left_with_margins_on(state.ids.frame, 18.0, 10.0)
197 .font_id(self.fonts.cyri.conrod_id)
198 .font_size(self.fonts.cyri.scale(14))
199 .color(TEXT_COLOR)
200 .set(state.ids.online_txt, ui);
201 Text::new(&player_count.to_string())
202 .right_from(state.ids.online_txt, 5.0)
203 .font_id(self.fonts.cyri.conrod_id)
204 .font_size(self.fonts.cyri.scale(14))
205 .color(TEXT_COLOR)
206 .set(state.ids.online_no, ui);
207 if state.ids.player_names.len() < player_count {
209 state.update(|s| {
210 s.ids
211 .player_rows
212 .resize(player_count, &mut ui.widget_id_generator());
213 s.ids
214 .player_names
215 .resize(player_count, &mut ui.widget_id_generator());
216 s.ids
217 .player_pvp_icons
218 .resize(player_count, &mut ui.widget_id_generator());
219 s.ids
220 .player_mod_badges
221 .resize(player_count, &mut ui.widget_id_generator());
222 })
223 };
224
225 let my_uid = self.client.uid();
227 let mut player_list = players
228 .filter(|(uid, _)| Some(**uid) != my_uid)
229 .filter(|(_, player)| {
230 self.show
231 .social_search_key
232 .as_ref()
233 .map(|search_key| {
234 search_key
235 .to_lowercase()
236 .split_whitespace()
237 .all(|substring| {
238 let player_alias = &player.player_alias.to_lowercase();
239 let character_name = player
240 .character
241 .as_ref()
242 .map(|character| character.name.to_lowercase());
243 player_alias.contains(substring)
244 || character_name
245 .map(|cn| cn.contains(substring))
246 .unwrap_or(false)
247 })
248 })
249 .unwrap_or(true)
250 })
251 .collect_vec();
252 player_list.sort_by_key(|(_, player)| {
253 player
254 .character
255 .as_ref()
256 .map(|character| &character.name)
257 .unwrap_or(&player.player_alias)
258 .to_lowercase()
259 });
260 for (i, (&uid, player_info)) in player_list.into_iter().enumerate() {
261 let hide_username = true;
262 let selected = state.selected_uid.is_some_and(|u| u.0 == uid);
263 let alias = &player_info.player_alias;
264 let name_text = match &player_info.character {
265 Some(character) => {
266 if hide_username {
267 character.name.to_string()
268 } else {
269 format!("[{}] {}", alias, &character.name)
270 }
271 },
272 None => format!(
273 "{} [{}]",
274 alias,
275 self.localized_strings.get_msg("hud-group-in_menu")
276 ), };
278 let name_text_length_limited = if name_text.len() > 29 {
279 format!("{}...", name_text.chars().take(26).collect::<String>())
280 } else {
281 name_text
282 };
283 let acc_name_txt = format!(
284 "{}: {}",
285 &self.localized_strings.get_msg("hud-social-account"),
286 alias
287 );
288 let button = Button::image(if !selected {
290 self.imgs.nothing
291 } else {
292 self.imgs.selection
293 })
294 .hover_image(if selected {
295 self.imgs.selection
296 } else {
297 self.imgs.selection_hover
298 })
299 .press_image(if selected {
300 self.imgs.selection
301 } else {
302 self.imgs.selection_press
303 })
304 .w_h(256.0, 20.0)
305 .image_color(color::rgba(1.0, 0.82, 0.27, 1.0));
306 let button = if i == 0 {
307 button.mid_top_with_margin_on(state.ids.online_align, 1.0)
308 } else {
309 button.down_from(state.ids.player_names[i - 1], 1.0)
310 };
311 if button
312 .label(&name_text_length_limited)
313 .label_font_size(self.fonts.cyri.scale(14))
314 .label_y(conrod_core::position::Relative::Scalar(1.0))
315 .label_font_id(self.fonts.cyri.conrod_id)
316 .label_color(TEXT_COLOR)
317 .depth(1.0)
318 .with_tooltip(
319 self.tooltip_manager,
320 &acc_name_txt,
321 "",
322 &button_tooltip,
323 TEXT_COLOR,
324 )
325 .set(state.ids.player_names[i], ui)
326 .was_clicked()
327 {
328 state.update(|s| s.selected_uid = Some((uid, Instant::now())));
329 }
330
331 if i % 2 != 0 {
333 Rectangle::fill_with([300.0, 20.0], color::rgba(1.0, 1.0, 1.0, 0.025))
334 .middle_of(state.ids.player_names[i])
335 .depth(2.0)
336 .set(state.ids.player_rows[i], ui);
337 }
338
339 if player_info.is_moderator {
341 Image::new(self.imgs.chat_moderator_badge)
342 .w_h(20.0, 20.0)
343 .right_from(state.ids.player_names[i], 0.0)
344 .with_tooltip(
345 self.tooltip_manager,
346 "",
347 "This player is a moderator.",
348 &button_tooltip,
349 TEXT_COLOR,
350 )
351 .set(state.ids.player_mod_badges[i], ui);
352 }
353
354 if let BattleMode::PvP = player_info.battle_mode {
356 Image::new(self.imgs.player_pvp)
357 .w_h(20.0, 20.0)
358 .left_from(state.ids.player_names[i], 0.0)
359 .with_tooltip(
360 self.tooltip_manager,
361 "",
362 "This player has PvP enabled.",
363 &button_tooltip,
364 TEXT_COLOR,
365 )
366 .set(state.ids.player_pvp_icons[i], ui);
367 }
368 }
369
370 let is_leader_or_not_in_group = self
372 .client
373 .group_info()
374 .is_none_or(|(_, l_uid)| self.client.uid() == Some(l_uid));
375
376 let current_members = self
377 .client
378 .group_members()
379 .iter()
380 .filter(|(_, role)| matches!(role, group::Role::Member))
381 .count()
382 + 1;
383 let current_invites = self.client.pending_invites().len();
384 let max_members = self.client.max_group_size() as usize;
385 let group_not_full = current_members + current_invites < max_members;
386 let selected_to_invite = (is_leader_or_not_in_group && group_not_full)
387 .then(|| {
388 state
389 .selected_uid
390 .as_ref()
391 .map(|(s, _)| *s)
392 .filter(|selected| {
393 self.client
394 .player_list()
395 .get(selected)
396 .is_some_and(|selected_player| {
397 selected_player.is_online && selected_player.character.is_some()
398 })
399 })
400 .or_else(|| {
401 self.selected_entity
402 .and_then(|s| self.client.state().read_component_copied(s.0))
403 })
404 .filter(|selected| {
405 !self.client.group_members().contains_key(selected)
407 })
408 })
409 .flatten();
410
411 let invite_text = self.localized_strings.get_msg("hud-group-invite");
412 let invite_button = Button::image(self.imgs.button)
413 .w_h(106.0, 26.0)
414 .left_from(
415 match battle_mode {
416 BattleMode::PvE => state.ids.pvp_button_off,
417 BattleMode::PvP => state.ids.pvp_button_on,
418 },
419 5.0,
420 )
421 .hover_image(if selected_to_invite.is_some() {
422 self.imgs.button_hover
423 } else {
424 self.imgs.button
425 })
426 .press_image(if selected_to_invite.is_some() {
427 self.imgs.button_press
428 } else {
429 self.imgs.button
430 })
431 .label(&invite_text)
432 .label_y(conrod_core::position::Relative::Scalar(3.0))
433 .label_color(if selected_to_invite.is_some() {
434 TEXT_COLOR
435 } else {
436 TEXT_COLOR_3
437 })
438 .image_color(if selected_to_invite.is_some() {
439 TEXT_COLOR
440 } else {
441 TEXT_COLOR_3
442 })
443 .label_font_size(self.fonts.cyri.scale(15))
444 .label_font_id(self.fonts.cyri.conrod_id);
445
446 if if self.client.group_info().is_some() {
447 let tooltip_txt = format!(
448 "{}/{} {}",
449 current_members + current_invites,
450 max_members,
451 &self.localized_strings.get_msg("hud-group-members")
452 );
453 invite_button
454 .with_tooltip(
455 self.tooltip_manager,
456 &tooltip_txt,
457 "",
458 &button_tooltip,
459 TEXT_COLOR,
460 )
461 .set(state.ids.invite_button, ui)
462 } else {
463 invite_button.set(state.ids.invite_button, ui)
464 }
465 .was_clicked()
466 {
467 if let Some(uid) = selected_to_invite {
468 events.push(Event::Invite(uid));
469 state.update(|s| {
470 s.selected_uid = None;
471 });
472 }
473 }
474
475 if Button::image(self.imgs.search_btn)
477 .top_left_with_margins_on(state.ids.frame, 54.0, 9.0)
478 .hover_image(self.imgs.search_btn_hover)
479 .press_image(self.imgs.search_btn_press)
480 .w_h(16.0, 16.0)
481 .set(state.ids.player_search_icon, ui)
482 .was_clicked()
483 {
484 events.push(Event::Focus(state.ids.player_search_input));
485 }
486 Rectangle::fill([248.0, 20.0])
487 .top_left_with_margins_on(state.ids.player_search_icon, -2.0, 18.0)
488 .hsla(0.0, 0.0, 0.0, 0.7)
489 .depth(1.0)
490 .parent(state.ids.bg)
491 .set(state.ids.player_search_input_bg, ui);
492 if let Some(string) =
493 TextEdit::new(self.show.social_search_key.as_deref().unwrap_or_default())
494 .top_left_with_margins_on(state.ids.player_search_icon, -1.0, 22.0)
495 .w_h(215.0, 20.0)
496 .font_id(self.fonts.cyri.conrod_id)
497 .font_size(self.fonts.cyri.scale(14))
498 .color(TEXT_COLOR)
499 .set(state.ids.player_search_input, ui)
500 {
501 events.push(Event::SearchPlayers(Some(string)));
502 }
503 Rectangle::fill_with([266.0, 20.0], color::TRANSPARENT)
504 .top_left_with_margins_on(state.ids.player_search_icon, -1.0, 0.0)
505 .graphics_for(state.ids.player_search_icon)
506 .set(state.ids.player_search_input_overlay, ui);
507
508 let pvp_tooltip = Tooltip::new({
509 let edge = &self.rot_imgs.tt_side;
510 let corner = &self.rot_imgs.tt_corner;
511 ImageFrame::new(
512 [edge.cw180, edge.none, edge.cw270, edge.cw90],
513 [corner.none, corner.cw270, corner.cw90, corner.cw180],
514 Color::Rgba(0.08, 0.07, 0.04, 1.0),
515 5.0,
516 )
517 })
518 .title_font_size(self.fonts.cyri.scale(15))
519 .parent(ui.window)
520 .desc_font_size(self.fonts.cyri.scale(12))
521 .font_id(self.fonts.cyri.conrod_id)
522 .desc_text_color(TEXT_COLOR);
523
524 match battle_mode {
526 BattleMode::PvE => {
527 if Button::image(self.imgs.pvp_off)
528 .w_h(26.0, 26.0)
529 .bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0)
530 .with_tooltip(
531 self.tooltip_manager,
532 "",
533 "PvP is off. Click to enable PvP.",
534 &pvp_tooltip,
535 TEXT_COLOR,
536 )
537 .set(state.ids.pvp_button_off, ui)
538 .was_clicked()
539 {
540 events.push(Event::SetBattleMode(BattleMode::PvP))
541 };
542 },
543 BattleMode::PvP => {
544 if Button::image(self.imgs.pvp_on)
545 .w_h(26.0, 26.0)
546 .bottom_right_with_margins_on(state.ids.frame, 9.0, 7.0)
547 .with_tooltip(
548 self.tooltip_manager,
549 "",
550 "PvP is on. Click to disable PvP.",
551 &pvp_tooltip,
552 TEXT_COLOR,
553 )
554 .set(state.ids.pvp_button_on, ui)
555 .was_clicked()
556 {
557 events.push(Event::SetBattleMode(BattleMode::PvE))
558 }
559 },
560 }
561
562 events
563 }
564}