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