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 hud::slots::SlotKind,
10 ui::{
11 ItemTooltip, ItemTooltipManager, ItemTooltipable,
12 fonts::Fonts,
13 slot::{ContentSize, SlotMaker},
14 },
15 window::{LastInput, MenuInput},
16};
17use client::Client;
18use common::{
19 assets::AssetExt,
20 comp::{
21 Inventory,
22 inventory::slot::Slot,
23 item::{ItemDef, ItemDesc, ItemI18n, Quality},
24 },
25};
26use conrod_core::{
27 Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
28 builder_methods, color,
29 widget::{self, Button, Rectangle, Text},
30 widget_ids,
31};
32use i18n::Localization;
33use specs::Entity as EcsEntity;
34use std::{borrow::Borrow, sync::Arc};
35use vek::Vec2;
36
37pub enum SlotEvents {
38 ChangeLocalFocus(usize),
39 Close,
40}
41
42#[derive(WidgetCommon)]
43pub struct SlotGrid<'a> {
44 #[conrod(common_builder)]
45 common: widget::CommonBuilder,
46 client: &'a Client,
47 imgs: &'a Imgs,
48 item_imgs: &'a ItemImgs,
49 fonts: &'a Fonts,
50 item_tooltip_manager: &'a mut ItemTooltipManager,
51 slot_manager: &'a mut SlotManager,
52 inventory: &'a Inventory,
53 item_tooltip: &'a ItemTooltip<'a>,
54 localized_strings: &'a Localization,
55 item_i18n: &'a ItemI18n,
56 entity: EcsEntity,
57 last_input: &'a LastInput,
58 pulse: f32,
59 menu_events: &'a Vec<MenuInput>,
60 active_content: usize,
61 is_us: bool,
62 details_mode: bool,
63 show_salvage: bool,
64 columns: usize,
65 spacing: f64,
66 slot_size: f64,
67}
68
69widget_ids! {
70 struct Ids {
71 inventory_slots[],
72 inv_slot_names[],
73 inv_slot_amounts[],
74
75 context_menu,
76 }
77}
78
79pub struct State {
80 ids: Ids,
81
82 active_context_slot: Option<SlotKind>,
83 context_menu_pos: [f64; 2],
85 active_slot: [usize; 2],
86}
87
88impl<'a> SlotGrid<'a> {
89 builder_methods! {
90 pub columns { columns = usize }
91 pub spacing { spacing = f64 }
92 pub slot_size { slot_size = f64 }
93 pub is_us { is_us = bool }
94 pub details_mode { details_mode = bool }
95 pub show_salvage { show_salvage = bool }
96 }
97
98 pub fn new(
99 client: &'a Client,
100 imgs: &'a Imgs,
101 item_imgs: &'a ItemImgs,
102 fonts: &'a Fonts,
103 item_tooltip_manager: &'a mut ItemTooltipManager,
104 slot_manager: &'a mut SlotManager,
105 inventory: &'a Inventory,
106 item_tooltip: &'a ItemTooltip<'a>,
107 localized_strings: &'a Localization,
108 item_i18n: &'a ItemI18n,
109 entity: EcsEntity,
110 last_input: &'a LastInput,
111 pulse: f32,
112 menu_events: &'a Vec<MenuInput>,
113 active_content: usize,
114 ) -> Self {
115 SlotGrid {
116 common: widget::CommonBuilder::default(),
117 client,
118 imgs,
119 item_imgs,
120 fonts,
121 item_tooltip_manager,
122 slot_manager,
123 inventory,
124 item_tooltip,
125 localized_strings,
126 item_i18n,
127 entity,
128 last_input,
129 pulse,
130 menu_events,
131 active_content,
132 is_us: true,
133 details_mode: false,
134 show_salvage: false,
135 columns: 6,
136 slot_size: 55.0,
137 spacing: 6.0,
138 }
139 }
140}
141
142impl<'a> Widget for SlotGrid<'a> {
143 type Event = Vec<SlotEvents>;
144 type State = State;
145 type Style = ();
146
147 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
148 State {
149 ids: Ids::new(id_gen),
150 active_context_slot: None,
151 context_menu_pos: [0.0, 0.0],
152 active_slot: [0, 0],
153 }
154 }
155
156 fn style(&self) -> Self::Style {}
157
158 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
159 let widget::UpdateArgs { id, state, ui, .. } = args;
160
161 let selected = self.slot_manager.selected();
163 if selected.is_none() {
164 state.update(|s| {
165 s.active_context_slot = None;
167 })
168 }
169
170 let total_slots = self.inventory.capacity() + self.inventory.overflow_items().count();
172
173 let cols = if self.details_mode { 1 } else { self.columns };
175
176 let mut events = Vec::new();
177
178 let mut clicked = false;
187 if selected.is_none() && self.active_content == 0 {
188 for event in self.menu_events {
189 match *event {
190 MenuInput::Up => state.update(|s| {
191 let [x, y] = s.active_slot;
192 if y > 0 {
193 s.active_slot = [x, y - 1];
194 }
195 }),
196 MenuInput::Down => state.update(|s| {
197 let [x, y] = s.active_slot;
198 if y < (total_slots / cols) {
199 s.active_slot = [x, y + 1];
200 }
201 }),
202 MenuInput::Left => state.update(|s| {
203 let [x, y] = s.active_slot;
204 if x > 0 {
205 s.active_slot = [x - 1, y];
206 }
207 }),
208 MenuInput::Right => state.update(|s| {
209 let [x, y] = s.active_slot;
210 if x < self.columns - 1 && (y * cols) + (x + 1) < total_slots {
212 s.active_slot = [x + 1, y];
213 }
214 }),
215 MenuInput::LocalFocus => {
216 events.push(SlotEvents::ChangeLocalFocus(1));
217 },
218 MenuInput::Apply => {
219 clicked = true;
220 },
221 MenuInput::Back => {
222 events.push(SlotEvents::Close);
223 },
224 _ => {},
225 }
226 }
227 }
228
229 if state.ids.inventory_slots.len() < self.inventory.capacity() {
231 state.update(|s| {
232 s.ids.inventory_slots.resize(
233 self.inventory.capacity() + self.inventory.overflow_items().count(),
234 &mut ui.widget_id_generator(),
235 );
236 s.ids
237 .inv_slot_names
238 .resize(self.inventory.capacity(), &mut ui.widget_id_generator());
239 s.ids
240 .inv_slot_amounts
241 .resize(self.inventory.capacity(), &mut ui.widget_id_generator());
242 });
243 }
244
245 let mouseover_loadout_slots = self
248 .slot_manager
249 .mouse_over_slot
250 .and_then(|x| {
251 if let SlotKind::Equip(e) = x {
252 self.inventory.get_slot_range_for_equip_slot(e)
253 } else {
254 None
255 }
256 })
257 .unwrap_or(0usize..0usize);
258
259 let mut slot_maker = SlotMaker {
261 empty_slot: self.imgs.inv_slot,
262 hovered_slot: self.imgs.skillbar_index,
263 filled_slot: self.imgs.inv_slot,
264 selected_slot: self.imgs.inv_slot_sel,
265 background_color: Some(UI_MAIN),
266 content_size: ContentSize {
267 width_height_ratio: 1.0,
268 max_fraction: 0.75,
269 },
270 selected_content_scale: 1.067,
271 amount_font: self.fonts.cyri.conrod_id,
272 amount_margins: Vec2::new(-4.0, 0.0),
273 amount_font_size: self.fonts.cyri.scale(12),
274 amount_text_color: TEXT_COLOR,
275 content_source: self.inventory,
276 image_source: self.item_imgs,
277 slot_manager: Some(self.slot_manager),
278 last_input: self.last_input,
279 pulse: self.pulse,
280 };
281
282 let mut items = self
283 .inventory
284 .slots_with_id()
285 .map(|(slot, item)| (Slot::Inventory(slot), item.as_ref()))
286 .chain(
287 self.inventory
288 .overflow_items()
289 .enumerate()
290 .map(|(i, item)| (Slot::Overflow(i), Some(item))),
291 )
292 .collect::<Vec<_>>();
293 if self.details_mode && !self.is_us {
294 items.sort_by_cached_key(|(_, item)| {
295 (
296 item.is_none(),
297 item.as_ref().map(|i| {
298 (
299 std::cmp::Reverse(i.quality()),
300 {
301 let (name, _) =
303 util::item_text(i, self.localized_strings, self.item_i18n);
304 name
305 },
306 i.amount(),
307 )
308 }),
309 )
310 });
311 }
312
313 for (i, (pos, item)) in items.into_iter().enumerate() {
314 if self.details_mode && !self.is_us && item.is_none() {
315 continue;
316 }
317 let (x, y) = if self.details_mode {
318 (0, i)
319 } else {
320 (i % self.columns, i / self.columns)
321 };
322
323 let x_pos = (x as f64 * (self.slot_size + self.spacing)).floor();
325 let y_pos = (y as f64 * (self.slot_size + self.spacing)).floor();
326 let inv_slot = InventorySlot {
327 slot: pos,
328 ours: self.is_us,
329 entity: self.entity,
330 };
331
332 let menu_hover = state.active_slot[0] == x
334 && state.active_slot[1] == y && selected.is_none() && self.active_content == 0; let mut slot_widget = slot_maker
339 .fabricate(inv_slot, [self.slot_size as f32; 2], menu_hover, clicked)
340 .top_left_with_margins_on(
341 id,
342 (y as f64 * (self.slot_size + self.spacing)).floor(),
345 (x as f64 * (self.slot_size + self.spacing)).floor(),
346 );
347
348 if mouseover_loadout_slots.contains(&i) {
350 slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0));
351 }
352
353 if self.show_salvage && item.as_ref().is_some_and(|item| item.is_salvageable()) {
354 slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0));
355 }
356
357 if matches!(pos, Slot::Overflow(_)) {
359 slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 0.0, 0.0, 1.0));
360 }
361
362 if let Some(item) = item {
363 let quality_col_img = match item.quality() {
364 Quality::Low => self.imgs.inv_slot_grey,
365 Quality::Common => self.imgs.inv_slot_common,
366 Quality::Moderate => self.imgs.inv_slot_green,
367 Quality::High => self.imgs.inv_slot_blue,
368 Quality::Epic => self.imgs.inv_slot_purple,
369 Quality::Legendary => self.imgs.inv_slot_gold,
370 Quality::Artifact => self.imgs.inv_slot_orange,
371 _ => self.imgs.inv_slot_red,
372 };
373
374 let prices_info = self
375 .client
376 .pending_trade()
377 .as_ref()
378 .and_then(|(_, _, prices)| prices.clone());
379
380 if self.show_salvage && item.is_salvageable() {
381 let salvage_result: Vec<_> = item
382 .salvage_output()
383 .map(|(material_id, _)| Arc::<ItemDef>::load_expect_cloned(material_id))
384 .map(|item| item as Arc<dyn ItemDesc>)
385 .collect();
386
387 let items = salvage_result
388 .iter()
389 .map(|item| item.borrow())
390 .chain(core::iter::once(item as &dyn ItemDesc));
391
392 slot_widget
393 .filled_slot(quality_col_img)
394 .with_item_tooltip(
395 self.item_tooltip_manager,
396 items,
397 &prices_info,
398 self.item_tooltip,
399 )
400 .set(state.ids.inventory_slots[i], ui);
401 } else {
402 slot_widget
403 .filled_slot(quality_col_img)
404 .with_item_tooltip(
405 self.item_tooltip_manager,
406 core::iter::once(item as &dyn ItemDesc),
407 &prices_info,
408 self.item_tooltip,
409 )
410 .set(state.ids.inventory_slots[i], ui);
411 }
412 if self.details_mode {
413 let (name, _) = util::item_text(item, self.localized_strings, self.item_i18n);
414 Text::new(&name)
416 .top_left_with_margins_on(
417 id,
418 0.0 + y as f64 * self.slot_size,
419 30.0 + x as f64 * self.slot_size,
420 )
421 .font_id(self.fonts.cyri.conrod_id)
422 .font_size(self.fonts.cyri.scale(14))
423 .color(color::WHITE)
424 .set(state.ids.inv_slot_names[i], ui);
425
426 let col = self.columns;
427 let size = self.columns;
428 let space = self.spacing as usize;
429 let current_width = ((col * size) + ((col - 1) * space)) as f64;
430 Text::new(&format!("{}", item.amount()))
431 .top_left_with_margins_on(
432 id,
433 0.0 + y as f64 * self.slot_size,
434 current_width - 40.0_f64 * self.slot_size,
435 )
436 .font_id(self.fonts.cyri.conrod_id)
437 .font_size(self.fonts.cyri.scale(14))
438 .color(color::WHITE)
439 .set(state.ids.inv_slot_amounts[i], ui);
440 }
441 } else {
442 slot_widget.set(state.ids.inventory_slots[i], ui);
443 }
444
445 if selected == Some(inv_slot.into()) {
447 state.update(|s| {
448 s.active_context_slot = selected;
449 let menu_width = 130.0;
450 let offset = if x < self.columns / 2 {
451 self.slot_size } else {
453 -menu_width };
455 s.context_menu_pos = [x_pos + offset, y_pos];
456 });
457 }
458 }
459
460 if state.active_context_slot.is_some() {
462 let context_use = self.localized_strings.get_msg("hud-context-menu-use");
463 let context_drop = self.localized_strings.get_msg("hud-context-menu-drop");
466 let context_cancel = self.localized_strings.get_msg("hud-context-menu-cancel");
467
468 let actions = [context_use, context_drop, context_cancel];
469 let [x, y] = state.context_menu_pos;
471 let total_h = (actions.len() as f64 * 25.0) + ((actions.len() as f64 + 1.0) * 2.0);
472
473 let event = ContextMenu::new(
474 &actions,
475 self.fonts,
476 self.imgs,
477 self.menu_events,
478 self.last_input,
479 )
480 .top_left_with_margins_on(id, y, x)
481 .w_h(130.0, total_h)
482 .set(state.ids.context_menu, ui);
483
484 if let Some(index) = event {
485 match index {
486 0 => self.slot_manager.use_selected(),
487 1 => self.slot_manager.dropped_selected(),
488 2 => self.slot_manager.idle(),
489 _ => self.slot_manager.idle(),
490 }
491
492 state.update(|s| s.active_context_slot = None);
493 }
494 }
495
496 events
497 }
498}
499
500#[derive(WidgetCommon)]
501struct ContextMenu<'a, T: 'a + AsRef<str>> {
502 #[conrod(common_builder)]
503 common: widget::CommonBuilder,
504 actions: &'a [T],
505 fonts: &'a Fonts,
506 imgs: &'a Imgs,
507 menu_events: &'a Vec<MenuInput>,
508 last_input: &'a LastInput,
509}
510
511widget_ids! {
512 struct ContextMenuIds {
513 bg,
514 buttons[],
515 }
516}
517
518struct ContextState {
519 ids: ContextMenuIds,
520
521 active_slot: usize,
522}
523
524impl<'a, T: AsRef<str>> ContextMenu<'a, T> {
525 fn new(
526 actions: &'a [T],
527 fonts: &'a Fonts,
528 imgs: &'a Imgs,
529 menu_events: &'a Vec<MenuInput>,
530 last_input: &'a LastInput,
531 ) -> Self {
532 ContextMenu {
533 common: widget::CommonBuilder::default(),
534 actions,
535 fonts,
536 imgs,
537 menu_events,
538 last_input,
539 }
540 }
541}
542
543impl<'a, T: AsRef<str>> Widget for ContextMenu<'a, T> {
544 type Event = Option<usize>;
545 type State = ContextState;
546 type Style = ();
547
548 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
549 ContextState {
550 ids: ContextMenuIds::new(id_gen),
551 active_slot: 0,
552 }
553 }
554
555 fn style(&self) -> Self::Style {}
556
557 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
558 let widget::UpdateArgs {
559 id,
560 state,
561 ui,
562 rect,
563 ..
564 } = args;
565 let mut clicked_index = None;
566
567 let item_h = 25.0;
568 let spacing = 2.0;
569 let actions_len = self.actions.len();
570
571 let mut clicked = false;
577 for event in self.menu_events {
578 match *event {
579 MenuInput::Up => state.update(|s| {
580 let y = s.active_slot;
581 if y > 0 {
582 s.active_slot = y - 1;
583 }
584 }),
585 MenuInput::Down => state.update(|s| {
586 let y = s.active_slot;
587 if y < actions_len - 1 {
588 s.active_slot = y + 1;
589 }
590 }),
591 MenuInput::Apply => {
592 clicked = true;
593 },
594 MenuInput::Back => {
595 clicked_index = Some(actions_len - 1);
597 },
598 _ => {},
599 }
600 }
601
602 Rectangle::fill_with(rect.dim(), Color::Rgba(0.2, 0.2, 0.2, 0.99))
604 .middle_of(id)
605 .set(state.ids.bg, ui);
606
607 if state.ids.buttons.len() < actions_len {
608 state.update(|s| {
609 s.ids
610 .buttons
611 .resize(actions_len, &mut ui.widget_id_generator());
612 });
613 }
614
615 for (i, label) in self.actions.iter().enumerate() {
617 let btn_id = state.ids.buttons[i];
618 let active_btn = state.active_slot == i;
619 let btn = Button::image(self.imgs.nothing)
620 .color(color::BLACK)
621 .border(20.0)
622 .border_color(
623 if active_btn && (*self.last_input == LastInput::Keyboard || *self.last_input == LastInput::Controller) {
624 color::YELLOW
625 } else {
626 color::WHITE
627 }
628 )
629 .label(label.as_ref())
630 .label_font_size(self.fonts.cyri.scale(12))
631 .label_font_id(self.fonts.cyri.conrod_id)
632 .label_color(
633 if active_btn && (*self.last_input == LastInput::Keyboard || *self.last_input == LastInput::Controller) {
634 color::YELLOW
635 } else {
636 color::WHITE
637 }
638 )
639 .hover_image(self.imgs.selection_hover) .press_image(self.imgs.selection_press)
641 .image_color(color::rgba(1.0, 0.82, 0.27, 1.0))
642 .h(item_h)
643 .w(rect.w() - (spacing * 2.0))
644 .parent(state.ids.bg);
645
646 let placed_btn = if i == 0 {
647 btn.mid_top_with_margin_on(state.ids.bg, spacing)
648 } else {
649 btn.down_from(state.ids.buttons[i - 1], spacing)
650 };
651
652 if placed_btn.set(btn_id, ui).was_clicked() || (clicked && active_btn) {
653 clicked_index = Some(i);
654 }
655 }
656
657 clicked_index
658 }
659}