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