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