1use super::{
2 TEXT_COLOR, UI_MAIN,
3 img_ids::Imgs,
4 item_imgs::ItemImgs,
5 slots::{InventorySlot, SlotManager},
6 util,
7};
8use crate::{
9 GlobalState,
10 hud::slots::SlotKind,
11 ui::{
12 ItemTooltip, ItemTooltipManager, ItemTooltipable,
13 fonts::Fonts,
14 slot::{ContentSize, SlotMaker},
15 },
16 window::MenuInput,
17};
18use client::Client;
19use common::{
20 assets::AssetExt,
21 comp::{
22 Inventory,
23 inventory::slot::Slot,
24 item::{ItemDef, ItemDesc, ItemI18n, ItemKind, Quality},
25 },
26};
27use conrod_core::{
28 Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
29 builder_methods, color,
30 widget::{self, Button, Rectangle, Text},
31 widget_ids,
32};
33use i18n::Localization;
34use specs::Entity as EcsEntity;
35use std::{borrow::Borrow, sync::Arc};
36use vek::Vec2;
37
38#[derive(PartialEq)]
39pub enum TabFilters {
40 Gear,
41 None,
45}
46
47pub enum SlotEvents {
48 Close,
49 ExitUp,
50 ExitLeft,
51 ExitRight,
52 ExitDown,
53 FilteredSize(usize),
54}
55
56#[derive(WidgetCommon)]
57pub struct SlotGrid<'a> {
58 #[conrod(common_builder)]
59 common: widget::CommonBuilder,
60 client: &'a Client,
61 global_state: &'a GlobalState,
62 imgs: &'a Imgs,
63 item_imgs: &'a ItemImgs,
64 fonts: &'a Fonts,
65 item_tooltip_manager: &'a mut ItemTooltipManager,
66 slot_manager: &'a mut SlotManager,
67 inventory: &'a Inventory,
68 item_tooltip: &'a ItemTooltip<'a>,
69 localized_strings: &'a Localization,
70 item_i18n: &'a ItemI18n,
71 entity: EcsEntity,
72 pulse: f32,
73 menu_events: &'a Vec<MenuInput>,
74 is_us: bool,
75 details_mode: bool,
76 show_salvage: bool,
77 columns: usize,
78 spacing: f64,
79 slot_size: f64,
80 is_focused: bool,
81 filter: TabFilters,
82}
83
84widget_ids! {
85 struct Ids {
86 item_slots[],
87 inv_slot_names[],
88 inv_slot_amounts[],
89
90 context_menu,
91 spacing_below,
92 }
93}
94
95pub struct State {
96 ids: Ids,
97
98 active_context_slot: Option<SlotKind>,
99 context_menu_pos: [f64; 2],
101 active_slot: [usize; 2],
102}
103
104impl<'a> SlotGrid<'a> {
105 builder_methods! {
106 pub columns { columns = usize }
107 pub spacing { spacing = f64 }
108 pub slot_size { slot_size = f64 }
109 pub is_focused { is_focused = bool }
110 pub filter { filter = TabFilters }
111 }
112
113 #[expect(clippy::too_many_arguments)]
114 pub fn new(
115 client: &'a Client,
116 global_state: &'a GlobalState,
117 imgs: &'a Imgs,
118 item_imgs: &'a ItemImgs,
119 fonts: &'a Fonts,
120 item_tooltip_manager: &'a mut ItemTooltipManager,
121 slot_manager: &'a mut SlotManager,
122 inventory: &'a Inventory,
123 item_tooltip: &'a ItemTooltip<'a>,
124 localized_strings: &'a Localization,
125 item_i18n: &'a ItemI18n,
126 entity: EcsEntity,
127 pulse: f32,
128 menu_events: &'a Vec<MenuInput>,
129 is_us: bool,
130 details_mode: bool,
131 show_salvage: bool,
132 ) -> Self {
133 SlotGrid {
134 common: widget::CommonBuilder::default(),
135 client,
136 global_state,
137 imgs,
138 item_imgs,
139 fonts,
140 item_tooltip_manager,
141 slot_manager,
142 inventory,
143 item_tooltip,
144 localized_strings,
145 item_i18n,
146 entity,
147 pulse,
148 menu_events,
149 is_us,
150 details_mode,
151 show_salvage,
152 columns: 6,
153 slot_size: 55.0,
154 spacing: 6.0,
155 is_focused: true,
156 filter: TabFilters::None,
157 }
158 }
159}
160
161impl<'a> Widget for SlotGrid<'a> {
162 type Event = Vec<SlotEvents>;
163 type State = State;
164 type Style = ();
165
166 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
167 State {
168 ids: Ids::new(id_gen),
169 active_context_slot: None,
170 context_menu_pos: [0.0, 0.0],
171 active_slot: [0, 0],
172 }
173 }
174
175 fn style(&self) -> Self::Style {}
176
177 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
178 let widget::UpdateArgs { id, state, ui, .. } = args;
179
180 let selected = self.slot_manager.selected();
182 if selected.is_none() {
183 state.update(|s| {
184 s.active_context_slot = None;
186 })
187 }
188
189 let inventory_iter = || {
191 self.inventory
192 .slots_with_id()
193 .map(|(slot, item)| (Slot::Inventory(slot), item.as_ref()))
194 .chain(
195 self.inventory
196 .overflow_items()
197 .enumerate()
198 .map(|(i, item)| (Slot::Overflow(i), Some(item))),
199 )
200 };
201 let mut items = inventory_iter()
202 .filter(|(_, items_list)| match self.filter {
203 TabFilters::Gear => {
204 if let Some(item) = items_list {
205 matches!(
206 &*item.kind(),
207 ItemKind::Tool(_)
208 | ItemKind::ModularComponent(_)
209 | ItemKind::Lantern(_)
210 | ItemKind::Armor(_)
211 | ItemKind::Glider
212 )
213 } else {
214 false
215 }
216 },
217 TabFilters::None => true,
250 })
251 .collect::<Vec<_>>();
252 if self.details_mode && !self.is_us {
253 items.sort_by_cached_key(|(_, item)| {
254 (
255 item.is_none(),
256 item.as_ref().map(|i| {
257 (
258 std::cmp::Reverse(i.quality()),
259 {
260 let (name, _) =
262 util::item_text(i, self.localized_strings, self.item_i18n);
263 name
264 },
265 i.amount(),
266 )
267 }),
268 )
269 });
270 }
271
272 if self.filter != TabFilters::None
275 && let Some(empty_slot) = inventory_iter().find(|(_, item)| item.is_none())
276 {
277 items.push(empty_slot);
278 }
279
280 let total_slots = items.len();
282 let cols = if self.details_mode { 1 } else { self.columns };
283
284 let mut events = Vec::new();
285
286 let mut clicked = false;
295 if selected.is_none() && self.is_focused {
296 for event in self.menu_events {
297 match *event {
298 MenuInput::Up => state.update(|s| {
299 let [x, y] = s.active_slot;
300 if y > 0 {
301 s.active_slot = [x, y - 1];
302 } else {
303 events.push(SlotEvents::ExitUp);
304 }
305 }),
306 MenuInput::Down => state.update(|s| {
307 let [x, y] = s.active_slot;
308 if y < (total_slots / cols) {
309 s.active_slot = [x, y + 1];
310 } else {
311 events.push(SlotEvents::ExitDown);
312 }
313 }),
314 MenuInput::Left => state.update(|s| {
315 let [x, y] = s.active_slot;
316 if x > 0 {
317 s.active_slot = [x - 1, y];
318 } else {
319 events.push(SlotEvents::ExitLeft);
320 }
321 }),
322 MenuInput::Right => state.update(|s| {
323 let [x, y] = s.active_slot;
324 if x < self.columns - 1 && (y * cols) + (x + 1) < total_slots {
326 s.active_slot = [x + 1, y];
327 } else {
328 events.push(SlotEvents::ExitRight);
329 }
330 }),
331 MenuInput::Apply => {
332 clicked = true;
333 },
334 MenuInput::Back => {
335 events.push(SlotEvents::Close);
336 },
337 _ => {},
338 }
339 }
340 }
341
342 if state.ids.item_slots.len() < total_slots {
344 state.update(|s| {
345 s.ids
346 .item_slots
347 .resize(total_slots, &mut ui.widget_id_generator());
348 s.ids
349 .inv_slot_names
350 .resize(total_slots, &mut ui.widget_id_generator());
351 s.ids
352 .inv_slot_amounts
353 .resize(total_slots, &mut ui.widget_id_generator());
354 });
355 }
356
357 let mouseover_loadout_slots = self
360 .slot_manager
361 .mouse_over_slot
362 .and_then(|x| {
363 if let SlotKind::Equip(e) = x {
364 self.inventory.get_slot_range_for_equip_slot(e)
365 } else {
366 None
367 }
368 })
369 .unwrap_or(0usize..0usize);
370
371 let mut slot_maker = SlotMaker {
373 empty_slot: self.imgs.inv_slot,
374 hovered_slot: self.imgs.skillbar_index,
375 filled_slot: self.imgs.inv_slot,
376 selected_slot: self.imgs.inv_slot_sel,
377 background_color: Some(UI_MAIN),
378 content_size: ContentSize {
379 width_height_ratio: 1.0,
380 max_fraction: 0.75,
381 },
382 selected_content_scale: 1.067,
383 amount_font: self.fonts.cyri.conrod_id,
384 amount_margins: Vec2::new(-4.0, 0.0),
385 amount_font_size: self.fonts.cyri.scale(12),
386 amount_text_color: TEXT_COLOR,
387 content_source: self.inventory,
388 image_source: self.item_imgs,
389 slot_manager: Some(self.slot_manager),
390 global_state: self.global_state,
391 pulse: self.pulse,
392 };
393
394 for (i, (pos, item)) in items.into_iter().enumerate() {
395 if self.details_mode && !self.is_us && item.is_none() {
396 continue;
397 }
398 let (x, y) = if self.details_mode {
399 (0, i)
400 } else {
401 (i % self.columns, i / self.columns)
402 };
403
404 let x_pos = (x as f64 * (self.slot_size + self.spacing)).floor();
406 let y_pos = (y as f64 * (self.slot_size + self.spacing)).floor();
407 let inv_slot = InventorySlot {
408 slot: pos,
409 ours: self.is_us,
410 entity: self.entity,
411 };
412
413 let menu_hover = state.active_slot[0] == x
415 && state.active_slot[1] == y && selected.is_none() && self.is_focused; let mut slot_widget = slot_maker
420 .fabricate(inv_slot, [self.slot_size as f32; 2], menu_hover, clicked)
421 .top_left_with_margins_on(
422 id,
423 (y as f64 * (self.slot_size + self.spacing)).floor(),
426 (x as f64 * (self.slot_size + self.spacing)).floor(),
427 );
428
429 if self.filter == TabFilters::None && mouseover_loadout_slots.contains(&i) {
431 slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0));
432 }
433
434 if self.show_salvage && item.as_ref().is_some_and(|item| item.is_salvageable()) {
435 slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0));
436 }
437
438 if matches!(pos, Slot::Overflow(_)) {
440 slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 0.0, 0.0, 1.0));
441 }
442
443 if let Some(item) = item {
444 let quality_col_img = match item.quality() {
445 Quality::Low => self.imgs.inv_slot_grey,
446 Quality::Common => self.imgs.inv_slot_common,
447 Quality::Moderate => self.imgs.inv_slot_green,
448 Quality::High => self.imgs.inv_slot_blue,
449 Quality::Epic => self.imgs.inv_slot_purple,
450 Quality::Legendary => self.imgs.inv_slot_gold,
451 Quality::Artifact => self.imgs.inv_slot_orange,
452 _ => self.imgs.inv_slot_red,
453 };
454
455 let prices_info = self
456 .client
457 .pending_trade()
458 .as_ref()
459 .and_then(|(_, _, prices)| prices.clone());
460
461 if self.show_salvage && item.is_salvageable() {
462 let salvage_result: Vec<_> = item
463 .salvage_output()
464 .map(|(material_id, _)| Arc::<ItemDef>::load_expect_cloned(material_id))
465 .map(|item| item as Arc<dyn ItemDesc>)
466 .collect();
467
468 let salvage_items = salvage_result
469 .iter()
470 .map(|item| item.borrow())
471 .chain(core::iter::once(item as &dyn ItemDesc));
472
473 slot_widget
474 .filled_slot(quality_col_img)
475 .with_item_tooltip(
476 self.item_tooltip_manager,
477 salvage_items,
478 &prices_info,
479 self.item_tooltip,
480 )
481 .set(state.ids.item_slots[i], ui);
482 } else {
483 slot_widget
484 .filled_slot(quality_col_img)
485 .with_item_tooltip(
486 self.item_tooltip_manager,
487 core::iter::once(item as &dyn ItemDesc),
488 &prices_info,
489 self.item_tooltip,
490 )
491 .set(state.ids.item_slots[i], ui);
492 }
493 if self.details_mode {
494 let (name, _) = util::item_text(item, self.localized_strings, self.item_i18n);
495 Text::new(&name)
497 .top_left_with_margins_on(
498 id,
499 0.0 + y as f64 * self.slot_size,
500 30.0 + x as f64 * self.slot_size,
501 )
502 .font_id(self.fonts.cyri.conrod_id)
503 .font_size(self.fonts.cyri.scale(14))
504 .color(color::WHITE)
505 .set(state.ids.inv_slot_names[i], ui);
506
507 let col = self.columns;
508 let size = self.columns;
509 let space = self.spacing as usize;
510 let current_width = ((col * size) + ((col - 1) * space)) as f64;
511 Text::new(&format!("{}", item.amount()))
512 .top_left_with_margins_on(
513 id,
514 0.0 + y as f64 * self.slot_size,
515 current_width - 40.0_f64 * self.slot_size,
516 )
517 .font_id(self.fonts.cyri.conrod_id)
518 .font_size(self.fonts.cyri.scale(14))
519 .color(color::WHITE)
520 .set(state.ids.inv_slot_amounts[i], ui);
521 }
522 } else {
523 slot_widget.set(state.ids.item_slots[i], ui);
524 }
525
526 if selected == Some(inv_slot.into()) {
528 state.update(|s| {
529 s.active_context_slot = selected;
530 let menu_width = 130.0;
531 let offset = if x < self.columns / 2 {
532 self.slot_size } else {
534 -menu_width };
536 s.context_menu_pos = [x_pos + offset, y_pos];
537 });
538 }
539 }
540
541 if !state.ids.item_slots.is_empty() {
543 Rectangle::fill_with([1.0, 15.0], color::TRANSPARENT)
544 .down_from(state.ids.item_slots[state.ids.item_slots.len() - 1], 0.0)
545 .set(state.ids.spacing_below, ui);
546 }
547
548 events.push(SlotEvents::FilteredSize(total_slots));
550
551 if state.active_context_slot.is_some() {
553 let context_use = self.localized_strings.get_msg("hud-context-menu-use");
554 let context_drop = self.localized_strings.get_msg("hud-context-menu-drop");
557 let context_cancel = self.localized_strings.get_msg("hud-context-menu-cancel");
558
559 let actions = [context_use, context_drop, context_cancel];
560 let [x, y] = state.context_menu_pos;
562 let total_h = (actions.len() as f64 * 25.0) + ((actions.len() as f64 + 1.0) * 2.0);
563
564 let event = ContextMenu::new(
565 self.global_state,
566 &actions,
567 self.fonts,
568 self.imgs,
569 self.menu_events,
570 )
571 .top_left_with_margins_on(id, y, x)
572 .w_h(130.0, total_h)
573 .set(state.ids.context_menu, ui);
574
575 if let Some(index) = event {
576 match index {
577 0 => self.slot_manager.use_selected(),
578 1 => self.slot_manager.dropped_selected(),
579 2 => self.slot_manager.idle(),
580 _ => self.slot_manager.idle(),
581 }
582
583 state.update(|s| s.active_context_slot = None);
584 }
585 }
586
587 events
588 }
589}
590
591#[derive(WidgetCommon)]
592struct ContextMenu<'a, T: 'a + AsRef<str>> {
593 #[conrod(common_builder)]
594 common: widget::CommonBuilder,
595 global_state: &'a GlobalState,
596 actions: &'a [T],
597 fonts: &'a Fonts,
598 imgs: &'a Imgs,
599 menu_events: &'a Vec<MenuInput>,
600}
601
602widget_ids! {
603 struct ContextMenuIds {
604 bg,
605 buttons[],
606 }
607}
608
609struct ContextState {
610 ids: ContextMenuIds,
611
612 active_slot: usize,
613}
614
615impl<'a, T: AsRef<str>> ContextMenu<'a, T> {
616 fn new(
617 global_state: &'a GlobalState,
618 actions: &'a [T],
619 fonts: &'a Fonts,
620 imgs: &'a Imgs,
621 menu_events: &'a Vec<MenuInput>,
622 ) -> Self {
623 ContextMenu {
624 common: widget::CommonBuilder::default(),
625 global_state,
626 actions,
627 fonts,
628 imgs,
629 menu_events,
630 }
631 }
632}
633
634impl<'a, T: AsRef<str>> Widget for ContextMenu<'a, T> {
635 type Event = Option<usize>;
636 type State = ContextState;
637 type Style = ();
638
639 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
640 ContextState {
641 ids: ContextMenuIds::new(id_gen),
642 active_slot: 0,
643 }
644 }
645
646 fn style(&self) -> Self::Style {}
647
648 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
649 let widget::UpdateArgs {
650 id,
651 state,
652 ui,
653 rect,
654 ..
655 } = args;
656 let mut clicked_index = None;
657
658 let item_h = 25.0;
659 let spacing = 2.0;
660 let actions_len = self.actions.len();
661
662 let mut clicked = false;
668 for event in self.menu_events {
669 match *event {
670 MenuInput::Up => state.update(|s| {
671 let y = s.active_slot;
672 if y > 0 {
673 s.active_slot = y - 1;
674 }
675 }),
676 MenuInput::Down => state.update(|s| {
677 let y = s.active_slot;
678 if y < actions_len - 1 {
679 s.active_slot = y + 1;
680 }
681 }),
682 MenuInput::Apply => {
683 clicked = true;
684 },
685 MenuInput::Back => {
686 clicked_index = Some(actions_len - 1);
688 },
689 _ => {},
690 }
691 }
692
693 Rectangle::fill_with(rect.dim(), Color::Rgba(0.2, 0.2, 0.2, 0.99))
695 .middle_of(id)
696 .set(state.ids.bg, ui);
697
698 if state.ids.buttons.len() < actions_len {
699 state.update(|s| {
700 s.ids
701 .buttons
702 .resize(actions_len, &mut ui.widget_id_generator());
703 });
704 }
705
706 for (i, label) in self.actions.iter().enumerate() {
708 let btn_id = state.ids.buttons[i];
709 let active_btn = state.active_slot == i;
710 let btn = Button::image(self.imgs.nothing)
711 .color(color::BLACK)
712 .border(20.0)
713 .border_color(
714 if active_btn && self.global_state.window.last_input_type_menu() {
715 color::YELLOW
716 } else {
717 color::WHITE
718 }
719 )
720 .label(label.as_ref())
721 .label_font_size(self.fonts.cyri.scale(12))
722 .label_font_id(self.fonts.cyri.conrod_id)
723 .label_color(
724 if active_btn && self.global_state.window.last_input_type_menu() {
725 color::YELLOW
726 } else {
727 color::WHITE
728 }
729 )
730 .hover_image(self.imgs.selection_hover) .press_image(self.imgs.selection_press)
732 .image_color(color::rgba(1.0, 0.82, 0.27, 1.0))
733 .h(item_h)
734 .w(rect.w() - (spacing * 2.0))
735 .parent(state.ids.bg);
736
737 let placed_btn = if i == 0 {
738 btn.mid_top_with_margin_on(state.ids.bg, spacing)
739 } else {
740 btn.down_from(state.ids.buttons[i - 1], spacing)
741 };
742
743 if placed_btn.set(btn_id, ui).was_clicked() || (clicked && active_btn) {
744 clicked_index = Some(i);
745 }
746 }
747
748 clicked_index
749 }
750}