Skip to main content

veloren_voxygen/hud/
bag.rs

1use super::{
2    CRITICAL_HP_COLOR, HudInfo, LOW_HP_COLOR, Show, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN, cr_color,
3    img_ids::{Imgs, ImgsRot},
4    item_imgs::ItemImgs,
5    slots::{ArmorSlot, EquipSlot, InventorySlot, SlotManager},
6    util,
7};
8use crate::{
9    GlobalState,
10    game_input::GameInput,
11    settings::{
12        HudPositionSettings,
13        hud_position::{
14            DEFAULT_OTHER_BAG_HEIGHT, DEFAULT_OTHER_BAG_WIDTH, DEFAULT_OWN_BAG_HEIGHT,
15            DEFAULT_OWN_BAG_WIDTH,
16        },
17    },
18    ui::{
19        ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable, Tooltip, TooltipManager,
20        Tooltipable,
21        fonts::Fonts,
22        slot::{ContentSize, SlotMaker},
23    },
24};
25use client::Client;
26use common::{
27    assets::AssetExt,
28    combat::{Damage, combat_rating, perception_dist_multiplier_from_stealth},
29    comp::{
30        Body, Energy, Health, Inventory, Poise, SkillSet, Stats,
31        inventory::{InventorySortOrder, slot::Slot},
32        item::{ItemDef, ItemDesc, ItemI18n, MaterialStatManifest, Quality},
33    },
34    recipe::RecipeBookManifest,
35};
36use conrod_core::{
37    Color, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, color,
38    widget::{self, Button, Image, Rectangle, Scrollbar, State as ConrodState, Text},
39    widget_ids,
40};
41use i18n::Localization;
42use std::borrow::Cow;
43
44use crate::hud::slots::SlotKind;
45use specs::Entity as EcsEntity;
46use std::{borrow::Borrow, sync::Arc};
47use vek::{Vec2, approx::AbsDiffEq};
48
49widget_ids! {
50    pub struct InventoryScrollerIds {
51        draggable_area,
52        inv_alignment,
53        inv_slots[],
54        inv_slot_names[],
55        inv_slot_amounts[],
56        //coin_ico,
57        space_txt,
58        //coin_txt,
59        inventory_title,
60        inventory_title_bg,
61        scrollbar_bg,
62        second_phase_scrollbar_bg,
63        scrollbar_slots,
64        left_scrollbar_slots,
65    }
66}
67
68pub struct InventoryScrollerState {
69    ids: InventoryScrollerIds,
70}
71
72pub enum InventoryScrollerEvent {
73    Drag(Vec2<f64>),
74}
75
76#[derive(WidgetCommon)]
77pub struct InventoryScroller<'a> {
78    client: &'a Client,
79    imgs: &'a Imgs,
80    item_imgs: &'a ItemImgs,
81    fonts: &'a Fonts,
82    #[conrod(common_builder)]
83    common: widget::CommonBuilder,
84    item_tooltip_manager: &'a mut ItemTooltipManager,
85    slot_manager: &'a mut SlotManager,
86    pulse: f32,
87    localized_strings: &'a Localization,
88    item_i18n: &'a ItemI18n,
89    show_stats: bool,
90    show_bag_inv: bool,
91    on_right: bool,
92    global_state: &'a GlobalState,
93    item_tooltip: &'a ItemTooltip<'a>,
94    playername: String,
95    entity: EcsEntity,
96    is_us: bool,
97    inventory: &'a Inventory,
98    bg_ids: &'a BackgroundIds,
99    show_salvage: bool,
100    details_mode: bool,
101}
102
103impl<'a> InventoryScroller<'a> {
104    #[expect(clippy::too_many_arguments)]
105    pub fn new(
106        client: &'a Client,
107        global_state: &'a GlobalState,
108        imgs: &'a Imgs,
109        item_imgs: &'a ItemImgs,
110        fonts: &'a Fonts,
111        item_tooltip_manager: &'a mut ItemTooltipManager,
112        slot_manager: &'a mut SlotManager,
113        pulse: f32,
114        localized_strings: &'a Localization,
115        item_i18n: &'a ItemI18n,
116        show_stats: bool,
117        show_bag_inv: bool,
118        on_right: bool,
119        item_tooltip: &'a ItemTooltip<'a>,
120        playername: String,
121        entity: EcsEntity,
122        is_us: bool,
123        inventory: &'a Inventory,
124        bg_ids: &'a BackgroundIds,
125        show_salvage: bool,
126        details_mode: bool,
127    ) -> Self {
128        InventoryScroller {
129            client,
130            imgs,
131            item_imgs,
132            fonts,
133            common: widget::CommonBuilder::default(),
134            item_tooltip_manager,
135            slot_manager,
136            pulse,
137            localized_strings,
138            item_i18n,
139            show_stats,
140            show_bag_inv,
141            on_right,
142            global_state,
143            item_tooltip,
144            playername,
145            entity,
146            is_us,
147            inventory,
148            bg_ids,
149            show_salvage,
150            details_mode,
151        }
152    }
153
154    fn background(&mut self, ui: &mut UiCell<'_>) {
155        let bag_settings = &self.global_state.settings.hud_position;
156        let bag_pos = if !self.on_right {
157            bag_settings.bag.other
158        } else {
159            bag_settings.bag.own
160        };
161
162        let bg_id = if !self.on_right {
163            self.imgs.inv_bg_bag
164        } else {
165            self.imgs.player_inv_bg_bag
166        };
167
168        let img_id = if !self.on_right {
169            self.imgs.inv_frame_bag
170        } else {
171            self.imgs.player_inv_frame_bag
172        };
173
174        let mut bg = Image::new(if self.show_stats {
175            self.imgs.inv_bg_stats
176        } else if self.show_bag_inv {
177            bg_id
178        } else {
179            self.imgs.inv_bg_armor
180        })
181        .w_h(
182            424.0,
183            if self.show_bag_inv && !self.on_right {
184                548.0
185            } else {
186                708.0
187            },
188        );
189
190        if self.on_right {
191            bg = bg.bottom_right_with_margins_on(ui.window, bag_pos.y, bag_pos.x);
192        } else {
193            bg = bg.bottom_left_with_margins_on(ui.window, bag_pos.y, bag_pos.x);
194        }
195
196        bg.color(Some(UI_MAIN)).set(self.bg_ids.bg, ui);
197
198        Image::new(if self.show_bag_inv {
199            img_id
200        } else {
201            self.imgs.inv_frame
202        })
203        .w_h(
204            424.0,
205            if self.show_bag_inv && !self.on_right {
206                548.0
207            } else {
208                708.0
209            },
210        )
211        .middle_of(self.bg_ids.bg)
212        .color(Some(UI_HIGHLIGHT_0))
213        .set(self.bg_ids.bg_frame, ui);
214    }
215
216    fn title(&mut self, state: &ConrodState<'_, InventoryScrollerState>, ui: &mut UiCell<'_>) {
217        Text::new(
218            &self
219                .localized_strings
220                .get_msg_ctx("hud-bag-inventory", &i18n::fluent_args! {
221                    "playername" => &*self.playername,
222                }),
223        )
224        .mid_top_with_margin_on(self.bg_ids.bg_frame, 9.0)
225        .font_id(self.fonts.cyri.conrod_id)
226        .font_size(self.fonts.cyri.scale(22))
227        .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
228        .set(state.ids.inventory_title_bg, ui);
229        Text::new(
230            &self
231                .localized_strings
232                .get_msg_ctx("hud-bag-inventory", &i18n::fluent_args! {
233                    "playername" => &*self.playername,
234                }),
235        )
236        .top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0)
237        .font_id(self.fonts.cyri.conrod_id)
238        .font_size(self.fonts.cyri.scale(22))
239        .color(TEXT_COLOR)
240        .set(state.ids.inventory_title, ui);
241    }
242
243    fn scrollbar_and_slots(
244        &mut self,
245        state: &mut ConrodState<'_, InventoryScrollerState>,
246        ui: &mut UiCell<'_>,
247    ) {
248        let space_max = self.inventory.slots().count();
249
250        // Slots Scrollbar
251        if space_max > 45 && !self.show_bag_inv {
252            // Scrollbar-BG
253            Image::new(self.imgs.scrollbar_bg)
254                .w_h(9.0, 173.0)
255                .bottom_right_with_margins_on(self.bg_ids.bg_frame, 42.0, 3.0)
256                .color(Some(UI_HIGHLIGHT_0))
257                .set(state.ids.scrollbar_bg, ui);
258            // Scrollbar
259            Scrollbar::y_axis(state.ids.inv_alignment)
260                .thickness(5.0)
261                .h(123.0)
262                .color(UI_MAIN)
263                .middle_of(state.ids.scrollbar_bg)
264                .set(state.ids.scrollbar_slots, ui);
265        } else if space_max > 135 && self.on_right {
266            // Scrollbar-BG
267            Image::new(self.imgs.scrollbar_bg_big)
268                .w_h(9.0, 592.0)
269                .bottom_right_with_margins_on(self.bg_ids.bg_frame, 42.0, 3.0)
270                .color(Some(UI_HIGHLIGHT_0))
271                .set(state.ids.scrollbar_bg, ui);
272            // Scrollbar
273            Scrollbar::y_axis(state.ids.inv_alignment)
274                .thickness(5.0)
275                .h(542.0)
276                .color(UI_MAIN)
277                .middle_of(state.ids.scrollbar_bg)
278                .set(state.ids.scrollbar_slots, ui);
279        };
280
281        // This is just for the offeror inventory scrollbar
282        if space_max >= 108 && !self.on_right && self.show_bag_inv {
283            // Left bag scrollbar background
284            Image::new(self.imgs.second_phase_scrollbar_bg)
285                .w_h(9.0, 434.0)
286                .bottom_right_with_margins_on(self.bg_ids.bg_frame, 42.0, 3.0)
287                .color(Some(UI_HIGHLIGHT_0))
288                .set(state.ids.second_phase_scrollbar_bg, ui);
289            // Left bag scrollbar
290            Scrollbar::y_axis(state.ids.inv_alignment)
291                .thickness(5.0)
292                .h(384.0)
293                .color(UI_MAIN)
294                .middle_of(state.ids.second_phase_scrollbar_bg)
295                .set(state.ids.left_scrollbar_slots, ui);
296        }
297
298        let grid_width = 362.0;
299        let grid_height = if self.show_bag_inv && !self.on_right {
300            440.0 // This for the left bag
301        } else if self.show_bag_inv && self.on_right {
302            600.0 // This for the expanded right bag
303        } else {
304            200.0
305        };
306
307        // Alignment for Grid
308        Rectangle::fill_with([grid_width, grid_height], color::TRANSPARENT)
309            .bottom_left_with_margins_on(
310                self.bg_ids.bg_frame,
311                29.0,
312                if self.show_bag_inv && !self.on_right {
313                    28.0
314                } else {
315                    46.5
316                },
317            )
318            .scroll_kids_vertically()
319            .set(state.ids.inv_alignment, ui);
320
321        // Bag Slots
322        // Create available inventory slot widgets
323        if state.ids.inv_slots.len() < self.inventory.capacity() {
324            state.update(|s| {
325                s.ids.inv_slots.resize(
326                    self.inventory.capacity() + self.inventory.overflow_items().count(),
327                    &mut ui.widget_id_generator(),
328                );
329            });
330        }
331        if state.ids.inv_slot_names.len() < self.inventory.capacity() {
332            state.update(|s| {
333                s.ids
334                    .inv_slot_names
335                    .resize(self.inventory.capacity(), &mut ui.widget_id_generator());
336            });
337        }
338        if state.ids.inv_slot_amounts.len() < self.inventory.capacity() {
339            state.update(|s| {
340                s.ids
341                    .inv_slot_amounts
342                    .resize(self.inventory.capacity(), &mut ui.widget_id_generator());
343            });
344        }
345        // Determine the range of inventory slots that are provided by the loadout item
346        // that the mouse is over
347        let mouseover_loadout_slots = self
348            .slot_manager
349            .mouse_over_slot
350            .and_then(|x| {
351                if let SlotKind::Equip(e) = x {
352                    self.inventory.get_slot_range_for_equip_slot(e)
353                } else {
354                    None
355                }
356            })
357            .unwrap_or(0usize..0usize);
358
359        // Display inventory contents
360        let mut slot_maker = SlotMaker {
361            empty_slot: self.imgs.inv_slot,
362            filled_slot: self.imgs.inv_slot,
363            selected_slot: self.imgs.inv_slot_sel,
364            background_color: Some(UI_MAIN),
365            content_size: ContentSize {
366                width_height_ratio: 1.0,
367                max_fraction: 0.75,
368            },
369            selected_content_scale: 1.067,
370            amount_font: self.fonts.cyri.conrod_id,
371            amount_margins: Vec2::new(-4.0, 0.0),
372            amount_font_size: self.fonts.cyri.scale(12),
373            amount_text_color: TEXT_COLOR,
374            content_source: self.inventory,
375            image_source: self.item_imgs,
376            slot_manager: Some(self.slot_manager),
377            pulse: self.pulse,
378        };
379
380        let mut i = 0;
381        let mut items = self
382            .inventory
383            .slots_with_id()
384            .map(|(slot, item)| (Slot::Inventory(slot), item.as_ref()))
385            .chain(
386                self.inventory
387                    .overflow_items()
388                    .enumerate()
389                    .map(|(i, item)| (Slot::Overflow(i), Some(item))),
390            )
391            .collect::<Vec<_>>();
392        if self.details_mode && !self.is_us {
393            items.sort_by_cached_key(|(_, item)| {
394                (
395                    item.is_none(),
396                    item.as_ref().map(|i| {
397                        (
398                            std::cmp::Reverse(i.quality()),
399                            {
400                                // TODO: we do double the work here, optimize?
401                                let (name, _) =
402                                    util::item_text(i, self.localized_strings, self.item_i18n);
403                                name
404                            },
405                            i.amount(),
406                        )
407                    }),
408                )
409            });
410        }
411        for (pos, item) in items.into_iter() {
412            if self.details_mode && !self.is_us && item.is_none() {
413                continue;
414            }
415            let (x, y) = if self.details_mode {
416                (0, i)
417            } else {
418                (i % 9, i / 9)
419            };
420            let slot_size = if self.details_mode { 20.0 } else { 40.0 };
421
422            // Slot
423            let mut slot_widget = slot_maker
424                .fabricate(
425                    InventorySlot {
426                        slot: pos,
427                        ours: self.is_us,
428                        entity: self.entity,
429                    },
430                    [slot_size as f32; 2],
431                )
432                .top_left_with_margins_on(
433                    state.ids.inv_alignment,
434                    0.0 + y as f64 * slot_size,
435                    0.0 + x as f64 * slot_size,
436                );
437
438            // Highlight slots are provided by the loadout item that the mouse is over
439            if mouseover_loadout_slots.contains(&i) {
440                slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0));
441            }
442
443            if self.show_salvage && item.as_ref().is_some_and(|item| item.is_salvageable()) {
444                slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0));
445            }
446
447            // Highlight in red slots that are overflow
448            if matches!(pos, Slot::Overflow(_)) {
449                slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 0.0, 0.0, 1.0));
450            }
451
452            if let Some(item) = item {
453                let quality_col_img = match item.quality() {
454                    Quality::Low => self.imgs.inv_slot_grey,
455                    Quality::Common => self.imgs.inv_slot_common,
456                    Quality::Moderate => self.imgs.inv_slot_green,
457                    Quality::High => self.imgs.inv_slot_blue,
458                    Quality::Epic => self.imgs.inv_slot_purple,
459                    Quality::Legendary => self.imgs.inv_slot_gold,
460                    Quality::Artifact => self.imgs.inv_slot_orange,
461                    _ => self.imgs.inv_slot_red,
462                };
463
464                let prices_info = self
465                    .client
466                    .pending_trade()
467                    .as_ref()
468                    .and_then(|(_, _, prices)| prices.clone());
469
470                if self.show_salvage && item.is_salvageable() {
471                    let salvage_result: Vec<_> = item
472                        .salvage_output()
473                        .map(|(material_id, _)| Arc::<ItemDef>::load_expect_cloned(material_id))
474                        .map(|item| item as Arc<dyn ItemDesc>)
475                        .collect();
476
477                    let items = salvage_result
478                        .iter()
479                        .map(|item| item.borrow())
480                        .chain(core::iter::once(item as &dyn ItemDesc));
481
482                    slot_widget
483                        .filled_slot(quality_col_img)
484                        .with_item_tooltip(
485                            self.item_tooltip_manager,
486                            items,
487                            &prices_info,
488                            self.item_tooltip,
489                        )
490                        .set(state.ids.inv_slots[i], ui);
491                } else {
492                    slot_widget
493                        .filled_slot(quality_col_img)
494                        .with_item_tooltip(
495                            self.item_tooltip_manager,
496                            core::iter::once(item as &dyn ItemDesc),
497                            &prices_info,
498                            self.item_tooltip,
499                        )
500                        .set(state.ids.inv_slots[i], ui);
501                }
502                if self.details_mode {
503                    let (name, _) = util::item_text(item, self.localized_strings, self.item_i18n);
504                    Text::new(&name)
505                        .top_left_with_margins_on(
506                            state.ids.inv_alignment,
507                            0.0 + y as f64 * slot_size,
508                            30.0 + x as f64 * slot_size,
509                        )
510                        .font_id(self.fonts.cyri.conrod_id)
511                        .font_size(self.fonts.cyri.scale(14))
512                        .color(color::WHITE)
513                        .set(state.ids.inv_slot_names[i], ui);
514
515                    Text::new(&format!("{}", item.amount()))
516                        .top_left_with_margins_on(
517                            state.ids.inv_alignment,
518                            0.0 + y as f64 * slot_size,
519                            grid_width - 40.0 + x as f64 * slot_size,
520                        )
521                        .font_id(self.fonts.cyri.conrod_id)
522                        .font_size(self.fonts.cyri.scale(14))
523                        .color(color::WHITE)
524                        .set(state.ids.inv_slot_amounts[i], ui);
525                }
526            } else {
527                slot_widget.set(state.ids.inv_slots[i], ui);
528            }
529            i += 1;
530        }
531    }
532
533    fn footer_metrics(
534        &mut self,
535        state: &ConrodState<'_, InventoryScrollerState>,
536        ui: &mut UiCell<'_>,
537    ) {
538        let space_used = self.inventory.populated_slots();
539        let space_max = self.inventory.slots().count();
540        let bag_space = format!("{}/{}", space_used, space_max);
541        let bag_space_percentage = space_used as f32 / space_max as f32;
542        //let coin_itemdef =
543        // Arc::<ItemDef>::load_expect_cloned("common.items.utility.coins"); let
544        // coin_count = self.inventory.item_count(&coin_itemdef); TODO: Reuse
545        // this to generally count a stackable item the player selected
546        // let cheese_itemdef =
547        // Arc::<ItemDef>::load_expect_cloned("common.items.food.cheese");
548        // let cheese_count = self.inventory.item_count(&cheese_itemdef);
549
550        // Coin Icon and Coin Text
551        /*Image::new(self.imgs.coin_ico)
552            .w_h(16.0, 17.0)
553            .bottom_left_with_margins_on(self.bg_ids.bg_frame, 2.0, 43.0)
554            .set(state.ids.coin_ico, ui);
555        Text::new(&format!("{}", coin_count))
556            .bottom_left_with_margins_on(self.bg_ids.bg_frame, 6.0, 64.0)
557            .font_id(self.fonts.cyri.conrod_id)
558            .font_size(self.fonts.cyri.scale(14))
559            .color(Color::Rgba(0.871, 0.863, 0.05, 1.0))
560            .set(state.ids.coin_txt, ui);*/
561        // TODO: Add a customizable counter for stackable items here
562        // TODO: Cheese is funny until it's real
563        /*Image::new(self.imgs.cheese_ico)
564            .w_h(16.0, 17.0)
565            .bottom_left_with_margins_on(self.bg_ids.bg_frame, 2.0, 110.0)
566            .set(state.ids.cheese_ico, ui);
567        Text::new(&format!("{}", cheese_count))
568            .bottom_left_with_margins_on(self.bg_ids.bg_frame, 6.0, 144.0)
569            .font_id(self.fonts.cyri.conrod_id)
570            .font_size(self.fonts.cyri.scale(14))
571            .color(Color::Rgba(0.871, 0.863, 0.05, 1.0))
572            .set(state.ids.cheese_txt, ui);*/
573        //Free Bag-Space
574        Text::new(&bag_space)
575            .bottom_right_with_margins_on(self.bg_ids.bg_frame, 6.0, 43.0)
576            .font_id(self.fonts.cyri.conrod_id)
577            .font_size(self.fonts.cyri.scale(14))
578            .color(if bag_space_percentage < 0.8 {
579                TEXT_COLOR
580            } else if bag_space_percentage < 1.0 {
581                LOW_HP_COLOR
582            } else {
583                CRITICAL_HP_COLOR
584            })
585            .set(state.ids.space_txt, ui);
586    }
587
588    fn draggable_area(
589        &self,
590        state: &ConrodState<'_, InventoryScrollerState>,
591        events: &mut Vec<InventoryScrollerEvent>,
592        ui: &mut UiCell<'_>,
593    ) {
594        let bag_settings = &self.global_state.settings.hud_position;
595        let bag_pos = if !self.on_right {
596            bag_settings.bag.other
597        } else {
598            bag_settings.bag.own
599        };
600
601        let bag_size: Vec2<f64> = if !self.on_right {
602            [DEFAULT_OTHER_BAG_WIDTH, DEFAULT_OTHER_BAG_HEIGHT].into()
603        } else {
604            [DEFAULT_OWN_BAG_WIDTH, DEFAULT_OWN_BAG_HEIGHT].into()
605        };
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 pos_delta: Vec2<f64> = if !self.on_right {
615            // Others (left side) bags use bottom_left_with_margins_on
616            pos_delta
617        } else {
618            // Own (right side) bags use bottom_right_with_margins_on
619            // which means we have to use positive margins to move left
620            // so we have to invert the x value from the delta.
621            pos_delta.with_x(-pos_delta.x)
622        };
623
624        let window_clamp = Vec2::new(ui.win_w, ui.win_h) - bag_size;
625
626        let new_pos = (bag_pos + pos_delta)
627            .map(|e| e.max(0.))
628            .map2(window_clamp, |e, bounds| e.min(bounds));
629
630        if new_pos.abs_diff_ne(&bag_pos, f64::EPSILON) {
631            events.push(InventoryScrollerEvent::Drag(new_pos));
632        }
633
634        if ui
635            .widget_input(state.ids.draggable_area)
636            .clicks()
637            .right()
638            .count()
639            == 1
640        {
641            events.push(InventoryScrollerEvent::Drag(if self.on_right {
642                HudPositionSettings::default().bag.own
643            } else {
644                HudPositionSettings::default().bag.other
645            }));
646        }
647
648        Rectangle::fill_with([424.0, 48.0], color::TRANSPARENT)
649            .top_left_with_margin_on(self.bg_ids.bg_frame, 0.0)
650            .set(state.ids.draggable_area, ui);
651    }
652}
653
654impl Widget for InventoryScroller<'_> {
655    type Event = Vec<InventoryScrollerEvent>;
656    type State = InventoryScrollerState;
657    type Style = ();
658
659    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
660        InventoryScrollerState {
661            ids: InventoryScrollerIds::new(id_gen),
662        }
663    }
664
665    fn style(&self) -> Self::Style {}
666
667    fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
668        let widget::UpdateArgs { state, ui, .. } = args;
669        let mut events = Vec::new();
670        self.background(ui);
671        self.title(state, ui);
672        self.scrollbar_and_slots(state, ui);
673        self.footer_metrics(state, ui);
674        if self
675            .global_state
676            .settings
677            .interface
678            .toggle_draggable_windows
679        {
680            self.draggable_area(state, &mut events, ui);
681        }
682        events
683    }
684}
685
686widget_ids! {
687    pub struct BackgroundIds {
688        bg,
689        bg_frame,
690    }
691}
692
693widget_ids! {
694    pub struct BagIds {
695        inventory_scroller,
696        bag_close,
697        char_ico,
698        inventory_sort,
699        inventory_sort_selected,
700        bag_expand_btn,
701        bag_details_btn,
702        // Armor Slots
703        head_slot,
704        neck_slot,
705        chest_slot,
706        shoulders_slot,
707        hands_slot,
708        legs_slot,
709        belt_slot,
710        lantern_slot,
711        ring1_slot,
712        ring2_slot,
713        feet_slot,
714        back_slot,
715        tabard_slot,
716        glider_slot,
717        active_mainhand_slot,
718        active_offhand_slot,
719        inactive_mainhand_slot,
720        inactive_offhand_slot,
721        swap_equipped_weapons_btn,
722        bag1_slot,
723        bag2_slot,
724        bag3_slot,
725        bag4_slot,
726        // Stats
727        stat_icons[],
728        stat_txts[],
729    }
730}
731
732#[derive(WidgetCommon)]
733pub struct Bag<'a> {
734    client: &'a Client,
735    info: &'a HudInfo<'a>,
736    global_state: &'a GlobalState,
737    imgs: &'a Imgs,
738    item_imgs: &'a ItemImgs,
739    fonts: &'a Fonts,
740    #[conrod(common_builder)]
741    common: widget::CommonBuilder,
742    rot_imgs: &'a ImgsRot,
743    tooltip_manager: &'a mut TooltipManager,
744    item_tooltip_manager: &'a mut ItemTooltipManager,
745    slot_manager: &'a mut SlotManager,
746    pulse: f32,
747    localized_strings: &'a Localization,
748    item_i18n: &'a ItemI18n,
749    stats: &'a Stats,
750    skill_set: &'a SkillSet,
751    health: &'a Health,
752    energy: &'a Energy,
753    show: &'a Show,
754    body: &'a Body,
755    msm: &'a MaterialStatManifest,
756    rbm: &'a RecipeBookManifest,
757    poise: &'a Poise,
758}
759
760impl<'a> Bag<'a> {
761    #[expect(clippy::too_many_arguments)]
762    pub fn new(
763        client: &'a Client,
764        info: &'a HudInfo,
765        global_state: &'a GlobalState,
766        imgs: &'a Imgs,
767        item_imgs: &'a ItemImgs,
768        fonts: &'a Fonts,
769        rot_imgs: &'a ImgsRot,
770        tooltip_manager: &'a mut TooltipManager,
771        item_tooltip_manager: &'a mut ItemTooltipManager,
772        slot_manager: &'a mut SlotManager,
773        pulse: f32,
774        localized_strings: &'a Localization,
775        item_i18n: &'a ItemI18n,
776        stats: &'a Stats,
777        skill_set: &'a SkillSet,
778        health: &'a Health,
779        energy: &'a Energy,
780        show: &'a Show,
781        body: &'a Body,
782        msm: &'a MaterialStatManifest,
783        rbm: &'a RecipeBookManifest,
784        poise: &'a Poise,
785    ) -> Self {
786        Self {
787            client,
788            info,
789            global_state,
790            imgs,
791            item_imgs,
792            fonts,
793            common: widget::CommonBuilder::default(),
794            rot_imgs,
795            tooltip_manager,
796            item_tooltip_manager,
797            slot_manager,
798            pulse,
799            localized_strings,
800            item_i18n,
801            stats,
802            skill_set,
803            energy,
804            health,
805            show,
806            body,
807            msm,
808            rbm,
809            poise,
810        }
811    }
812}
813const STATS: [&str; 6] = [
814    "Health",
815    "Energy",
816    "Protection",
817    "Combat Rating",
818    "Stun Resilience",
819    "Stealth",
820];
821
822pub struct BagState {
823    ids: BagIds,
824    bg_ids: BackgroundIds,
825}
826
827pub enum Event {
828    BagExpand,
829    Close,
830    ChangeInventorySortOrder(InventorySortOrder),
831    SortInventory(InventorySortOrder),
832    SwapEquippedWeapons,
833    SetDetailsMode(bool),
834    MoveBag(Vec2<f64>),
835}
836
837impl Widget for Bag<'_> {
838    type Event = Vec<Event>;
839    type State = BagState;
840    type Style = ();
841
842    fn init_state(&self, mut id_gen: widget::id::Generator) -> Self::State {
843        BagState {
844            bg_ids: BackgroundIds {
845                bg: id_gen.next(),
846                bg_frame: id_gen.next(),
847            },
848            ids: BagIds::new(id_gen),
849        }
850    }
851
852    fn style(&self) -> Self::Style {}
853
854    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
855        common_base::prof_span!("Bag::update");
856        let widget::UpdateArgs { state, ui, .. } = args;
857        let i18n = &self.localized_strings;
858
859        let mut events = Vec::new();
860        let bag_tooltip = Tooltip::new({
861            // Edge images [t, b, r, l]
862            // Corner images [tr, tl, br, bl]
863            let edge = &self.rot_imgs.tt_side;
864            let corner = &self.rot_imgs.tt_corner;
865            ImageFrame::new(
866                [edge.cw180, edge.none, edge.cw270, edge.cw90],
867                [corner.none, corner.cw270, corner.cw90, corner.cw180],
868                Color::Rgba(0.08, 0.07, 0.04, 1.0),
869                5.0,
870            )
871        })
872        .title_font_size(self.fonts.cyri.scale(15))
873        .parent(ui.window)
874        .desc_font_size(self.fonts.cyri.scale(12))
875        .font_id(self.fonts.cyri.conrod_id)
876        .desc_text_color(TEXT_COLOR);
877
878        if let Some(inventory) = self.client.inventories().get(self.info.viewpoint_entity) {
879            // Tooltips
880            let tooltip = Tooltip::new({
881                // Edge images [t, b, r, l]
882                // Corner images [tr, tl, br, bl]
883                let edge = &self.rot_imgs.tt_side;
884                let corner = &self.rot_imgs.tt_corner;
885                ImageFrame::new(
886                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
887                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
888                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
889                    5.0,
890                )
891            })
892            .title_font_size(self.fonts.cyri.scale(15))
893            .parent(ui.window)
894            .desc_font_size(self.fonts.cyri.scale(12))
895            .font_id(self.fonts.cyri.conrod_id)
896            .desc_text_color(TEXT_COLOR);
897
898            let item_tooltip = ItemTooltip::new(
899                {
900                    // Edge images [t, b, r, l]
901                    // Corner images [tr, tl, br, bl]
902                    let edge = &self.rot_imgs.tt_side;
903                    let corner = &self.rot_imgs.tt_corner;
904                    ImageFrame::new(
905                        [edge.cw180, edge.none, edge.cw270, edge.cw90],
906                        [corner.none, corner.cw270, corner.cw90, corner.cw180],
907                        Color::Rgba(0.08, 0.07, 0.04, 1.0),
908                        5.0,
909                    )
910                },
911                self.client,
912                self.info,
913                self.imgs,
914                self.item_imgs,
915                self.pulse,
916                self.msm,
917                self.rbm,
918                Some(inventory),
919                self.localized_strings,
920                self.item_i18n,
921            )
922            .title_font_size(self.fonts.cyri.scale(20))
923            .parent(ui.window)
924            .desc_font_size(self.fonts.cyri.scale(12))
925            .font_id(self.fonts.cyri.conrod_id)
926            .desc_text_color(TEXT_COLOR);
927
928            for event in InventoryScroller::new(
929                self.client,
930                self.global_state,
931                self.imgs,
932                self.item_imgs,
933                self.fonts,
934                self.item_tooltip_manager,
935                self.slot_manager,
936                self.pulse,
937                self.localized_strings,
938                self.item_i18n,
939                self.show.stats,
940                self.show.bag_inv,
941                true,
942                &item_tooltip,
943                self.localized_strings.get_content(&self.stats.name),
944                self.info.viewpoint_entity,
945                true,
946                inventory,
947                &state.bg_ids,
948                self.show.crafting_fields.salvage,
949                self.show.bag_details,
950            )
951            .set(state.ids.inventory_scroller, ui)
952            {
953                // Bubble events from the InventoryScroller widget
954                let InventoryScrollerEvent::Drag(pos) = event;
955                events.push(Event::MoveBag(pos));
956            }
957
958            // Char Pixel-Art
959            Image::new(self.imgs.char_art)
960                .w_h(40.0, 37.0)
961                .top_left_with_margins_on(state.bg_ids.bg, 4.0, 2.0)
962                .set(state.ids.char_ico, ui);
963
964            let buttons_top = if self.show.bag_inv { 53.0 } else { 460.0 };
965            let (txt, btn, hover, press) = if self.show.bag_details {
966                (
967                    "Grid mode",
968                    self.imgs.grid_btn,
969                    self.imgs.grid_btn_hover,
970                    self.imgs.grid_btn_press,
971                )
972            } else {
973                (
974                    "List mode",
975                    self.imgs.list_btn,
976                    self.imgs.list_btn_hover,
977                    self.imgs.list_btn_press,
978                )
979            };
980            let details_btn = Button::image(btn)
981                .w_h(32.0, 17.0)
982                .hover_image(hover)
983                .press_image(press);
984            if details_btn
985                .mid_top_with_margin_on(state.bg_ids.bg_frame, buttons_top)
986                .with_tooltip(self.tooltip_manager, txt, "", &bag_tooltip, TEXT_COLOR)
987                .set(state.ids.bag_details_btn, ui)
988                .was_clicked()
989            {
990                events.push(Event::SetDetailsMode(!self.show.bag_details));
991            }
992            // Button to expand bag
993            let (txt, btn, hover, press) = if self.show.bag_inv {
994                (
995                    "Show Loadout",
996                    self.imgs.collapse_btn,
997                    self.imgs.collapse_btn_hover,
998                    self.imgs.collapse_btn_press,
999                )
1000            } else {
1001                (
1002                    "Expand Bag",
1003                    self.imgs.expand_btn,
1004                    self.imgs.expand_btn_hover,
1005                    self.imgs.expand_btn_press,
1006                )
1007            };
1008            let expand_btn = Button::image(btn)
1009                .w_h(30.0, 17.0)
1010                .hover_image(hover)
1011                .press_image(press);
1012
1013            // Only show expand button when it's needed...
1014            if (inventory.slots().count() > 45 || self.show.bag_inv)
1015                && expand_btn
1016                    .top_right_with_margins_on(state.bg_ids.bg_frame, buttons_top, 37.0)
1017                    .with_tooltip(self.tooltip_manager, txt, "", &bag_tooltip, TEXT_COLOR)
1018                    .set(state.ids.bag_expand_btn, ui)
1019                    .was_clicked()
1020            {
1021                events.push(Event::BagExpand);
1022            }
1023
1024            // Sort mode inventory button
1025            if Button::image(self.imgs.inv_sort_btn)
1026            .w_h(30.0, 17.0)
1027            .hover_image(self.imgs.inv_sort_btn_hover)
1028            .press_image(self.imgs.inv_sort_btn_press)
1029            .top_left_with_margins_on(state.bg_ids.bg_frame, buttons_top, 87.0) // 30 + 10 + 47
1030            .with_tooltip(
1031                self.tooltip_manager,
1032                &(match self.global_state.settings.inventory.sort_order.next() {
1033                    InventorySortOrder::Name => i18n.get_msg("hud-bag-change_to_sort_by_name"),
1034                    InventorySortOrder::Quality => i18n.get_msg("hud-bag-change_to_sort_by_quality"),
1035                    InventorySortOrder::Category => i18n.get_msg("hud-bag-change_to_sort_by_category"),
1036                    InventorySortOrder::Tag => i18n.get_msg("hud-bag-change_to_sort_by_tag"),
1037                    InventorySortOrder::Amount => i18n.get_msg("hud-bag-change_to_sort_by_quantity"),
1038                }),
1039                "",
1040                &tooltip,
1041                color::WHITE,
1042            )
1043            .set(state.ids.inventory_sort, ui)
1044            .was_clicked()
1045            {
1046                // cycle sorting mode
1047                events.push(Event::ChangeInventorySortOrder(
1048                    self.global_state.settings.inventory.sort_order.next(),
1049                ));
1050            }
1051            // Sort inventory button with selected mode
1052            if Button::image(self.imgs.inv_sort_selected_btn)
1053                .w_h(30.0, 17.0)
1054                .hover_image(self.imgs.inv_sort_selected_btn_hover)
1055                .press_image(self.imgs.inv_sort_selected_btn_press)
1056                .top_left_with_margins_on(state.bg_ids.bg_frame, buttons_top, 47.0)
1057                .with_tooltip(
1058                    self.tooltip_manager,
1059                    &(match self.global_state.settings.inventory.sort_order {
1060                        InventorySortOrder::Name => i18n.get_msg("hud-bag-sort_by_name"),
1061                        InventorySortOrder::Quality => i18n.get_msg("hud-bag-sort_by_quality"),
1062                        InventorySortOrder::Category => i18n.get_msg("hud-bag-sort_by_category"),
1063                        InventorySortOrder::Tag => i18n.get_msg("hud-bag-sort_by_tag"),
1064                        InventorySortOrder::Amount => i18n.get_msg("hud-bag-sort_by_quantity"),
1065                    }),
1066                    "",
1067                    &tooltip,
1068                    color::WHITE,
1069                )
1070                .set(state.ids.inventory_sort_selected, ui)
1071                .was_clicked()
1072            {
1073                events.push(Event::SortInventory(
1074                    self.global_state.settings.inventory.sort_order,
1075                ));
1076            }
1077
1078            // Armor Slots
1079            let mut slot_maker = SlotMaker {
1080                empty_slot: self.imgs.armor_slot_empty,
1081                filled_slot: self.imgs.armor_slot,
1082                selected_slot: self.imgs.armor_slot_sel,
1083                background_color: Some(UI_HIGHLIGHT_0),
1084                content_size: ContentSize {
1085                    width_height_ratio: 1.0,
1086                    max_fraction: 0.75, /* Changes the item image size by setting a maximum
1087                                         * fraction
1088                                         * of either the width or height */
1089                },
1090                selected_content_scale: 1.067,
1091                amount_font: self.fonts.cyri.conrod_id,
1092                amount_margins: Vec2::new(-4.0, 0.0),
1093                amount_font_size: self.fonts.cyri.scale(12),
1094                amount_text_color: TEXT_COLOR,
1095                content_source: inventory,
1096                image_source: self.item_imgs,
1097                slot_manager: Some(self.slot_manager),
1098                pulse: self.pulse,
1099            };
1100
1101            // NOTE: Yes, macros considered harmful.
1102            // Though, this code mutably captures two different fields of `self`
1103            // This works because it's different branches of if-let
1104            // so in reality borrow checker allows you to do this as you
1105            // capture only one field.
1106            //
1107            // The less impossible, but still tricky part is denote type of
1108            // `$slot_maker` which has 1 lifetype parameter and 3 type parameters
1109            // in such way that it implements all traits conrod needs.
1110            //
1111            // And final part is that this uses that much of arguments
1112            // that just by passing all of them, you will get about the same
1113            // amount of lines this macro has or even more.
1114            //
1115            // So considering how many times we copy-paste this code
1116            // and how easy this macro looks it sounds like lawful evil.
1117            //
1118            // What this actually does is checks if we have equipped item on this slot
1119            // and if we do, display item tooltip for it.
1120            // If not, just show text of slot name.
1121            macro_rules! set_tooltip {
1122                ($slot_maker:expr, $slot_id:expr, $slot:expr, $desc:expr) => {
1123                    if let Some(item) = inventory.equipped($slot) {
1124                        let manager = &mut *self.item_tooltip_manager;
1125                        $slot_maker
1126                            .with_item_tooltip(
1127                                manager,
1128                                core::iter::once(item as &dyn ItemDesc),
1129                                &None,
1130                                &item_tooltip,
1131                            )
1132                            .set($slot_id, ui)
1133                    } else {
1134                        let manager = &mut *self.tooltip_manager;
1135                        $slot_maker
1136                            .with_tooltip(manager, &i18n.get_msg($desc), "", &tooltip, color::WHITE)
1137                            .set($slot_id, ui)
1138                    }
1139                };
1140            }
1141
1142            let filled_slot = self.imgs.armor_slot;
1143            if !self.show.bag_inv {
1144                // Stat icons and text
1145                state.update(|s| {
1146                    s.ids
1147                        .stat_icons
1148                        .resize(STATS.len(), &mut ui.widget_id_generator())
1149                });
1150                state.update(|s| {
1151                    s.ids
1152                        .stat_txts
1153                        .resize(STATS.len(), &mut ui.widget_id_generator())
1154                });
1155                // Stats
1156                let combat_rating = combat_rating(
1157                    inventory,
1158                    self.health,
1159                    self.energy,
1160                    self.poise,
1161                    self.skill_set,
1162                    *self.body,
1163                    self.msm,
1164                )
1165                .min(999.9);
1166                let indicator_col = cr_color(combat_rating);
1167                for i in STATS.iter().copied().enumerate() {
1168                    let btn = Button::image(match i.1 {
1169                        "Health" => self.imgs.health_ico,
1170                        "Energy" => self.imgs.energy_ico,
1171                        "Combat Rating" => self.imgs.combat_rating_ico,
1172                        "Protection" => self.imgs.protection_ico,
1173                        "Stun Resilience" => self.imgs.stun_res_ico,
1174                        "Stealth" => self.imgs.stealth_rating_ico,
1175                        _ => self.imgs.nothing,
1176                    })
1177                    .w_h(20.0, 20.0)
1178                    .image_color(if i.1 == "Combat Rating" {
1179                        indicator_col
1180                    } else {
1181                        TEXT_COLOR
1182                    });
1183                    let protection_txt = format!(
1184                        "{}%",
1185                        (100.0
1186                            * Damage::compute_damage_reduction(
1187                                None,
1188                                Some(inventory),
1189                                Some(self.stats),
1190                                self.msm
1191                            )) as i32
1192                    );
1193                    let health_txt = format!("{}", self.health.maximum().round() as usize);
1194                    let energy_txt = format!("{}", self.energy.maximum().round() as usize);
1195                    let combat_rating_txt = format!("{}", (combat_rating * 10.0) as usize);
1196                    let stun_res_txt = format!(
1197                        "{}",
1198                        (100.0
1199                            * Poise::compute_poise_damage_reduction(
1200                                Some(inventory),
1201                                self.msm,
1202                                None,
1203                                Some(self.stats),
1204                            )) as i32
1205                    );
1206                    let stealth_txt = format!(
1207                        "{:.1}%",
1208                        ((1.0
1209                            - perception_dist_multiplier_from_stealth(
1210                                Some(inventory),
1211                                None,
1212                                self.msm
1213                            ))
1214                            * 100.0)
1215                    );
1216                    let btn = if i.0 == 0 {
1217                        btn.top_left_with_margins_on(state.bg_ids.bg_frame, 55.0, 10.0)
1218                    } else {
1219                        btn.down_from(state.ids.stat_icons[i.0 - 1], 7.0)
1220                    };
1221                    let tooltip_head = match i.1 {
1222                        "Health" => i18n.get_msg("hud-bag-health"),
1223                        "Energy" => i18n.get_msg("hud-bag-energy"),
1224                        "Combat Rating" => i18n.get_msg("hud-bag-combat_rating"),
1225                        "Protection" => i18n.get_msg("hud-bag-protection"),
1226                        "Stun Resilience" => i18n.get_msg("hud-bag-stun_res"),
1227                        "Stealth" => i18n.get_msg("hud-bag-stealth"),
1228                        _ => Cow::Borrowed(""),
1229                    };
1230                    let tooltip_txt = match i.1 {
1231                        "Combat Rating" => i18n.get_msg("hud-bag-combat_rating_desc"),
1232                        "Protection" => i18n.get_msg("hud-bag-protection_desc"),
1233                        "Stun Resilience" => i18n.get_msg("hud-bag-stun_res_desc"),
1234                        _ => Cow::Borrowed(""),
1235                    };
1236                    btn.with_tooltip(
1237                        self.tooltip_manager,
1238                        &tooltip_head,
1239                        &tooltip_txt,
1240                        &bag_tooltip,
1241                        TEXT_COLOR,
1242                    )
1243                    .set(state.ids.stat_icons[i.0], ui);
1244                    Text::new(match i.1 {
1245                        "Health" => &health_txt,
1246                        "Energy" => &energy_txt,
1247                        "Combat Rating" => &combat_rating_txt,
1248                        "Protection" => &protection_txt,
1249                        "Stun Resilience" => &stun_res_txt,
1250                        "Stealth" => &stealth_txt,
1251                        _ => "",
1252                    })
1253                    .right_from(state.ids.stat_icons[i.0], 10.0)
1254                    .font_id(self.fonts.cyri.conrod_id)
1255                    .font_size(self.fonts.cyri.scale(14))
1256                    .color(TEXT_COLOR)
1257                    .graphics_for(state.ids.stat_icons[i.0])
1258                    .set(state.ids.stat_txts[i.0], ui);
1259                }
1260                // Loadout Slots
1261                //  Head
1262                let item_slot = EquipSlot::Armor(ArmorSlot::Head);
1263                let slot = slot_maker
1264                    .fabricate(item_slot, [45.0; 2])
1265                    .mid_top_with_margin_on(state.bg_ids.bg_frame, 60.0)
1266                    .with_icon(self.imgs.head_bg, Vec2::new(32.0, 40.0), Some(UI_MAIN))
1267                    .filled_slot(filled_slot);
1268
1269                let slot_id = state.ids.head_slot;
1270                set_tooltip!(slot, slot_id, item_slot, "hud-bag-head");
1271
1272                //  Necklace
1273                let item_slot = EquipSlot::Armor(ArmorSlot::Neck);
1274                let slot = slot_maker
1275                    .fabricate(item_slot, [45.0; 2])
1276                    .mid_bottom_with_margin_on(state.ids.head_slot, -55.0)
1277                    .with_icon(self.imgs.necklace_bg, Vec2::new(40.0, 31.0), Some(UI_MAIN))
1278                    .filled_slot(filled_slot);
1279
1280                let slot_id = state.ids.neck_slot;
1281                set_tooltip!(slot, slot_id, item_slot, "hud-bag-neck");
1282
1283                // Chest
1284                //Image::new(self.imgs.armor_slot) // different graphics for empty/non empty
1285                let item_slot = EquipSlot::Armor(ArmorSlot::Chest);
1286                let slot = slot_maker
1287                    .fabricate(item_slot, [85.0; 2])
1288                    .mid_bottom_with_margin_on(state.ids.neck_slot, -95.0)
1289                    .with_icon(self.imgs.chest_bg, Vec2::new(64.0, 42.0), Some(UI_MAIN))
1290                    .filled_slot(filled_slot);
1291
1292                let slot_id = state.ids.chest_slot;
1293                set_tooltip!(slot, slot_id, item_slot, "hud-bag-chest");
1294
1295                //  Shoulders
1296                let item_slot = EquipSlot::Armor(ArmorSlot::Shoulders);
1297                let slot = slot_maker
1298                    .fabricate(item_slot, [70.0; 2])
1299                    .bottom_left_with_margins_on(state.ids.chest_slot, 0.0, -80.0)
1300                    .with_icon(self.imgs.shoulders_bg, Vec2::new(60.0, 36.0), Some(UI_MAIN))
1301                    .filled_slot(filled_slot);
1302
1303                let slot_id = state.ids.shoulders_slot;
1304                set_tooltip!(slot, slot_id, item_slot, "hud-bag-shoulders");
1305
1306                // Hands
1307                let item_slot = EquipSlot::Armor(ArmorSlot::Hands);
1308                let slot = slot_maker
1309                    .fabricate(item_slot, [70.0; 2])
1310                    .bottom_right_with_margins_on(state.ids.chest_slot, 0.0, -80.0)
1311                    .with_icon(self.imgs.hands_bg, Vec2::new(55.0, 60.0), Some(UI_MAIN))
1312                    .filled_slot(filled_slot);
1313
1314                let slot_id = state.ids.hands_slot;
1315                set_tooltip!(slot, slot_id, item_slot, "hud-bag-hands");
1316
1317                // Belt
1318                let item_slot = EquipSlot::Armor(ArmorSlot::Belt);
1319                let slot = slot_maker
1320                    .fabricate(item_slot, [45.0; 2])
1321                    .mid_bottom_with_margin_on(state.ids.chest_slot, -55.0)
1322                    .with_icon(self.imgs.belt_bg, Vec2::new(40.0, 23.0), Some(UI_MAIN))
1323                    .filled_slot(filled_slot);
1324
1325                let slot_id = state.ids.belt_slot;
1326                set_tooltip!(slot, slot_id, item_slot, "hud-bag-belt");
1327
1328                // Legs
1329                let item_slot = EquipSlot::Armor(ArmorSlot::Legs);
1330                let slot = slot_maker
1331                    .fabricate(item_slot, [85.0; 2])
1332                    .mid_bottom_with_margin_on(state.ids.belt_slot, -95.0)
1333                    .with_icon(self.imgs.legs_bg, Vec2::new(48.0, 70.0), Some(UI_MAIN))
1334                    .filled_slot(filled_slot);
1335
1336                let slot_id = state.ids.legs_slot;
1337                set_tooltip!(slot, slot_id, item_slot, "hud-bag-legs");
1338
1339                // Ring
1340                let item_slot = EquipSlot::Armor(ArmorSlot::Ring1);
1341                let slot = slot_maker
1342                    .fabricate(item_slot, [45.0; 2])
1343                    .bottom_left_with_margins_on(state.ids.hands_slot, -55.0, 0.0)
1344                    .with_icon(self.imgs.ring_bg, Vec2::new(36.0, 40.0), Some(UI_MAIN))
1345                    .filled_slot(filled_slot);
1346
1347                let slot_id = state.ids.ring1_slot;
1348                set_tooltip!(slot, slot_id, item_slot, "hud-bag-ring");
1349
1350                // Ring 2
1351                let item_slot = EquipSlot::Armor(ArmorSlot::Ring2);
1352                let slot = slot_maker
1353                    .fabricate(item_slot, [45.0; 2])
1354                    .bottom_right_with_margins_on(state.ids.shoulders_slot, -55.0, 0.0)
1355                    .with_icon(self.imgs.ring_bg, Vec2::new(36.0, 40.0), Some(UI_MAIN))
1356                    .filled_slot(filled_slot);
1357
1358                let slot_id = state.ids.ring2_slot;
1359                set_tooltip!(slot, slot_id, item_slot, "hud-bag-ring");
1360
1361                // Back
1362                let item_slot = EquipSlot::Armor(ArmorSlot::Back);
1363                let slot = slot_maker
1364                    .fabricate(item_slot, [45.0; 2])
1365                    .down_from(state.ids.ring2_slot, 10.0)
1366                    .with_icon(self.imgs.back_bg, Vec2::new(33.0, 40.0), Some(UI_MAIN))
1367                    .filled_slot(filled_slot);
1368
1369                let slot_id = state.ids.back_slot;
1370                set_tooltip!(slot, slot_id, item_slot, "hud-bag-back");
1371
1372                // Foot
1373                let item_slot = EquipSlot::Armor(ArmorSlot::Feet);
1374                let slot = slot_maker
1375                    .fabricate(item_slot, [45.0; 2])
1376                    .down_from(state.ids.ring1_slot, 10.0)
1377                    .with_icon(self.imgs.feet_bg, Vec2::new(32.0, 40.0), Some(UI_MAIN))
1378                    .filled_slot(filled_slot);
1379
1380                let slot_id = state.ids.feet_slot;
1381                set_tooltip!(slot, slot_id, item_slot, "hud-bag-feet");
1382
1383                // Lantern
1384                let item_slot = EquipSlot::Lantern;
1385                let slot = slot_maker
1386                    .fabricate(item_slot, [45.0; 2])
1387                    .top_right_with_margins_on(state.bg_ids.bg_frame, 60.0, 5.0)
1388                    .with_icon(self.imgs.lantern_bg, Vec2::new(24.0, 38.0), Some(UI_MAIN))
1389                    .filled_slot(filled_slot);
1390
1391                let slot_id = state.ids.lantern_slot;
1392                set_tooltip!(slot, slot_id, item_slot, "hud-bag-lantern");
1393
1394                // Glider
1395                let item_slot = EquipSlot::Glider;
1396                let slot = slot_maker
1397                    .fabricate(item_slot, [45.0; 2])
1398                    .down_from(state.ids.lantern_slot, 5.0)
1399                    .with_icon(self.imgs.glider_bg, Vec2::new(38.0, 38.0), Some(UI_MAIN))
1400                    .filled_slot(filled_slot);
1401
1402                let slot_id = state.ids.glider_slot;
1403                set_tooltip!(slot, slot_id, item_slot, "hud-bag-glider");
1404
1405                // Tabard
1406                let item_slot = EquipSlot::Armor(ArmorSlot::Tabard);
1407                let slot = slot_maker
1408                    .fabricate(item_slot, [45.0; 2])
1409                    .down_from(state.ids.glider_slot, 5.0)
1410                    .with_icon(self.imgs.tabard_bg, Vec2::new(38.0, 38.0), Some(UI_MAIN))
1411                    .filled_slot(filled_slot);
1412
1413                let slot_id = state.ids.tabard_slot;
1414                set_tooltip!(slot, slot_id, item_slot, "hud-bag-tabard");
1415
1416                // Active Mainhand/Left-Slot
1417                let item_slot = EquipSlot::ActiveMainhand;
1418                let slot = slot_maker
1419                    .fabricate(item_slot, [85.0; 2])
1420                    .bottom_right_with_margins_on(state.ids.back_slot, -95.0, 0.0)
1421                    .with_icon(self.imgs.mainhand_bg, Vec2::new(75.0, 75.0), Some(UI_MAIN))
1422                    .filled_slot(filled_slot);
1423
1424                let slot_id = state.ids.active_mainhand_slot;
1425                set_tooltip!(slot, slot_id, item_slot, "hud-bag-mainhand");
1426
1427                // Active Offhand/Right-Slot
1428                let item_slot = EquipSlot::ActiveOffhand;
1429                let slot = slot_maker
1430                    .fabricate(item_slot, [85.0; 2])
1431                    .bottom_left_with_margins_on(state.ids.feet_slot, -95.0, 0.0)
1432                    .with_icon(self.imgs.offhand_bg, Vec2::new(75.0, 75.0), Some(UI_MAIN))
1433                    .filled_slot(filled_slot);
1434
1435                let slot_id = state.ids.active_offhand_slot;
1436                set_tooltip!(slot, slot_id, item_slot, "hud-bag-offhand");
1437
1438                // Inactive Mainhand/Left-Slot
1439                let item_slot = EquipSlot::InactiveMainhand;
1440                let slot = slot_maker
1441                    .fabricate(item_slot, [40.0; 2])
1442                    .bottom_right_with_margins_on(state.ids.active_mainhand_slot, 3.0, -47.0)
1443                    .with_icon(self.imgs.mainhand_bg, Vec2::new(35.0, 35.0), Some(UI_MAIN))
1444                    .filled_slot(filled_slot);
1445
1446                let slot_id = state.ids.inactive_mainhand_slot;
1447                set_tooltip!(slot, slot_id, item_slot, "hud-bag-inactive_mainhand");
1448
1449                // Inactive Offhand/Right-Slot
1450                let item_slot = EquipSlot::InactiveOffhand;
1451                let slot = slot_maker
1452                    .fabricate(item_slot, [40.0; 2])
1453                    .bottom_left_with_margins_on(state.ids.active_offhand_slot, 3.0, -47.0)
1454                    .with_icon(self.imgs.offhand_bg, Vec2::new(35.0, 35.0), Some(UI_MAIN))
1455                    .filled_slot(filled_slot);
1456
1457                let slot_id = state.ids.inactive_offhand_slot;
1458                set_tooltip!(slot, slot_id, item_slot, "hud-bag-inactive_offhand");
1459
1460                if Button::image(self.imgs.swap_equipped_weapons_btn)
1461                    .hover_image(self.imgs.swap_equipped_weapons_btn_hover)
1462                    .press_image(self.imgs.swap_equipped_weapons_btn_press)
1463                    .w_h(32.0, 40.0)
1464                    .bottom_left_with_margins_on(state.bg_ids.bg_frame, 0.0, 23.3)
1465                    .align_middle_y_of(state.ids.active_mainhand_slot)
1466                    .with_tooltip(
1467                        self.tooltip_manager,
1468                        &i18n.get_msg("hud-bag-swap_equipped_weapons_title"),
1469                        &(if let Some(key) = self
1470                            .global_state
1471                            .settings
1472                            .controls
1473                            .get_binding(GameInput::SwapLoadout)
1474                        {
1475                            i18n.get_msg_ctx(
1476                                "hud-bag-swap_equipped_weapons_desc",
1477                                &i18n::fluent_args! {
1478                                    "key" => key.display_string()
1479                                },
1480                            )
1481                        } else {
1482                            Cow::Borrowed("")
1483                        }),
1484                        &tooltip,
1485                        color::WHITE,
1486                    )
1487                    .set(state.ids.swap_equipped_weapons_btn, ui)
1488                    .was_clicked()
1489                {
1490                    events.push(Event::SwapEquippedWeapons);
1491                }
1492            }
1493
1494            // Bag 1
1495            let item_slot = EquipSlot::Armor(ArmorSlot::Bag1);
1496            let slot = slot_maker
1497                .fabricate(item_slot, [35.0; 2])
1498                .bottom_left_with_margins_on(
1499                    state.bg_ids.bg_frame,
1500                    if self.show.bag_inv { 600.0 } else { 167.0 },
1501                    3.0,
1502                )
1503                .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN))
1504                .filled_slot(filled_slot);
1505
1506            let slot_id = state.ids.bag1_slot;
1507            set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
1508
1509            // Bag 2
1510            let item_slot = EquipSlot::Armor(ArmorSlot::Bag2);
1511            let slot = slot_maker
1512                .fabricate(item_slot, [35.0; 2])
1513                .down_from(state.ids.bag1_slot, 2.0)
1514                .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN))
1515                .filled_slot(filled_slot);
1516
1517            let slot_id = state.ids.bag2_slot;
1518            set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
1519
1520            // Bag 3
1521            let item_slot = EquipSlot::Armor(ArmorSlot::Bag3);
1522            let slot = slot_maker
1523                .fabricate(item_slot, [35.0; 2])
1524                .down_from(state.ids.bag2_slot, 2.0)
1525                .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN))
1526                .filled_slot(filled_slot);
1527
1528            let slot_id = state.ids.bag3_slot;
1529            set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
1530
1531            // Bag 4
1532            let item_slot = EquipSlot::Armor(ArmorSlot::Bag4);
1533            let slot = slot_maker
1534                .fabricate(item_slot, [35.0; 2])
1535                .down_from(state.ids.bag3_slot, 2.0)
1536                .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN))
1537                .filled_slot(filled_slot);
1538
1539            let slot_id = state.ids.bag4_slot;
1540            set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
1541
1542            // Close button
1543            if Button::image(self.imgs.close_btn)
1544                .w_h(24.0, 25.0)
1545                .hover_image(self.imgs.close_btn_hover)
1546                .press_image(self.imgs.close_btn_press)
1547                .top_right_with_margins_on(state.bg_ids.bg, 0.0, 0.0)
1548                .set(state.ids.bag_close, ui)
1549                .was_clicked()
1550            {
1551                events.push(Event::Close);
1552            }
1553        }
1554
1555        events
1556    }
1557}