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