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        inventory_sort_selected,
618        scrollbar_bg,
619        scrollbar_slots,
620        tab_1,
621        tab_2,
622        tab_3,
623        tab_4,
624        bag_expand_btn,
625        bag_details_btn,
626        // Armor Slots
627        slots_bg,
628        head_slot,
629        neck_slot,
630        chest_slot,
631        shoulders_slot,
632        hands_slot,
633        legs_slot,
634        belt_slot,
635        lantern_slot,
636        ring1_slot,
637        ring2_slot,
638        feet_slot,
639        back_slot,
640        tabard_slot,
641        glider_slot,
642        active_mainhand_slot,
643        active_offhand_slot,
644        inactive_mainhand_slot,
645        inactive_offhand_slot,
646        swap_equipped_weapons_btn,
647        bag1_slot,
648        bag2_slot,
649        bag3_slot,
650        bag4_slot,
651        // Stats
652        stat_icons[],
653        stat_txts[],
654    }
655}
656
657#[derive(WidgetCommon)]
658pub struct Bag<'a> {
659    client: &'a Client,
660    info: &'a HudInfo<'a>,
661    global_state: &'a GlobalState,
662    imgs: &'a Imgs,
663    item_imgs: &'a ItemImgs,
664    fonts: &'a Fonts,
665    #[conrod(common_builder)]
666    common: widget::CommonBuilder,
667    rot_imgs: &'a ImgsRot,
668    tooltip_manager: &'a mut TooltipManager,
669    item_tooltip_manager: &'a mut ItemTooltipManager,
670    slot_manager: &'a mut SlotManager,
671    pulse: f32,
672    localized_strings: &'a Localization,
673    item_i18n: &'a ItemI18n,
674    stats: &'a Stats,
675    skill_set: &'a SkillSet,
676    health: &'a Health,
677    energy: &'a Energy,
678    show: &'a Show,
679    body: &'a Body,
680    msm: &'a MaterialStatManifest,
681    rbm: &'a RecipeBookManifest,
682    poise: &'a Poise,
683}
684
685impl<'a> Bag<'a> {
686    #[expect(clippy::too_many_arguments)]
687    pub fn new(
688        client: &'a Client,
689        info: &'a HudInfo,
690        global_state: &'a GlobalState,
691        imgs: &'a Imgs,
692        item_imgs: &'a ItemImgs,
693        fonts: &'a Fonts,
694        rot_imgs: &'a ImgsRot,
695        tooltip_manager: &'a mut TooltipManager,
696        item_tooltip_manager: &'a mut ItemTooltipManager,
697        slot_manager: &'a mut SlotManager,
698        pulse: f32,
699        localized_strings: &'a Localization,
700        item_i18n: &'a ItemI18n,
701        stats: &'a Stats,
702        skill_set: &'a SkillSet,
703        health: &'a Health,
704        energy: &'a Energy,
705        show: &'a Show,
706        body: &'a Body,
707        msm: &'a MaterialStatManifest,
708        rbm: &'a RecipeBookManifest,
709        poise: &'a Poise,
710    ) -> Self {
711        Self {
712            client,
713            info,
714            global_state,
715            imgs,
716            item_imgs,
717            fonts,
718            common: widget::CommonBuilder::default(),
719            rot_imgs,
720            tooltip_manager,
721            item_tooltip_manager,
722            slot_manager,
723            pulse,
724            localized_strings,
725            item_i18n,
726            stats,
727            skill_set,
728            energy,
729            health,
730            show,
731            body,
732            msm,
733            rbm,
734            poise,
735        }
736    }
737}
738const STATS: [&str; 6] = [
739    "Health",
740    "Energy",
741    "Protection",
742    "Combat Rating",
743    "Stun Resilience",
744    "Stealth",
745];
746
747pub struct BagState {
748    ids: BagIds,
749    bg_ids: BackgroundIds,
750}
751
752pub enum Event {
753    BagExpand,
754    Close,
755    ChangeInventorySortOrder(InventorySortOrder),
756    SortInventory(InventorySortOrder),
757    SwapEquippedWeapons,
758    SetDetailsMode(bool),
759}
760
761impl Widget for Bag<'_> {
762    type Event = Option<Event>;
763    type State = BagState;
764    type Style = ();
765
766    fn init_state(&self, mut id_gen: widget::id::Generator) -> Self::State {
767        BagState {
768            bg_ids: BackgroundIds {
769                bg: id_gen.next(),
770                bg_frame: id_gen.next(),
771            },
772            ids: BagIds::new(id_gen),
773        }
774    }
775
776    fn style(&self) -> Self::Style {}
777
778    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
779        common_base::prof_span!("Bag::update");
780        let widget::UpdateArgs { state, ui, .. } = args;
781        let i18n = &self.localized_strings;
782
783        let mut event = None;
784        let bag_tooltip = Tooltip::new({
785            // Edge images [t, b, r, l]
786            // Corner images [tr, tl, br, bl]
787            let edge = &self.rot_imgs.tt_side;
788            let corner = &self.rot_imgs.tt_corner;
789            ImageFrame::new(
790                [edge.cw180, edge.none, edge.cw270, edge.cw90],
791                [corner.none, corner.cw270, corner.cw90, corner.cw180],
792                Color::Rgba(0.08, 0.07, 0.04, 1.0),
793                5.0,
794            )
795        })
796        .title_font_size(self.fonts.cyri.scale(15))
797        .parent(ui.window)
798        .desc_font_size(self.fonts.cyri.scale(12))
799        .font_id(self.fonts.cyri.conrod_id)
800        .desc_text_color(TEXT_COLOR);
801        let inventories = self.client.inventories();
802        let inventory = inventories.get(self.info.viewpoint_entity)?;
803
804        // Tooltips
805        let tooltip = Tooltip::new({
806            // Edge images [t, b, r, l]
807            // Corner images [tr, tl, br, bl]
808            let edge = &self.rot_imgs.tt_side;
809            let corner = &self.rot_imgs.tt_corner;
810            ImageFrame::new(
811                [edge.cw180, edge.none, edge.cw270, edge.cw90],
812                [corner.none, corner.cw270, corner.cw90, corner.cw180],
813                Color::Rgba(0.08, 0.07, 0.04, 1.0),
814                5.0,
815            )
816        })
817        .title_font_size(self.fonts.cyri.scale(15))
818        .parent(ui.window)
819        .desc_font_size(self.fonts.cyri.scale(12))
820        .font_id(self.fonts.cyri.conrod_id)
821        .desc_text_color(TEXT_COLOR);
822
823        let item_tooltip = ItemTooltip::new(
824            {
825                // Edge images [t, b, r, l]
826                // Corner images [tr, tl, br, bl]
827                let edge = &self.rot_imgs.tt_side;
828                let corner = &self.rot_imgs.tt_corner;
829                ImageFrame::new(
830                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
831                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
832                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
833                    5.0,
834                )
835            },
836            self.client,
837            self.info,
838            self.imgs,
839            self.item_imgs,
840            self.pulse,
841            self.msm,
842            self.rbm,
843            Some(inventory),
844            self.localized_strings,
845            self.item_i18n,
846        )
847        .title_font_size(self.fonts.cyri.scale(20))
848        .parent(ui.window)
849        .desc_font_size(self.fonts.cyri.scale(12))
850        .font_id(self.fonts.cyri.conrod_id)
851        .desc_text_color(TEXT_COLOR);
852
853        InventoryScroller::new(
854            self.client,
855            self.imgs,
856            self.item_imgs,
857            self.fonts,
858            self.item_tooltip_manager,
859            self.slot_manager,
860            self.pulse,
861            self.localized_strings,
862            self.item_i18n,
863            self.show.stats,
864            self.show.bag_inv,
865            true,
866            &item_tooltip,
867            self.localized_strings.get_content(&self.stats.name),
868            self.info.viewpoint_entity,
869            true,
870            inventory,
871            &state.bg_ids,
872            self.show.crafting_fields.salvage,
873            self.show.bag_details,
874        )
875        .set(state.ids.inventory_scroller, ui);
876
877        // Char Pixel-Art
878        Image::new(self.imgs.char_art)
879            .w_h(40.0, 37.0)
880            .top_left_with_margins_on(state.bg_ids.bg, 4.0, 2.0)
881            .set(state.ids.char_ico, ui);
882
883        let buttons_top = if self.show.bag_inv { 53.0 } else { 460.0 };
884        let (txt, btn, hover, press) = if self.show.bag_details {
885            (
886                "Grid mode",
887                self.imgs.grid_btn,
888                self.imgs.grid_btn_hover,
889                self.imgs.grid_btn_press,
890            )
891        } else {
892            (
893                "List mode",
894                self.imgs.list_btn,
895                self.imgs.list_btn_hover,
896                self.imgs.list_btn_press,
897            )
898        };
899        let details_btn = Button::image(btn)
900            .w_h(32.0, 17.0)
901            .hover_image(hover)
902            .press_image(press);
903        if details_btn
904            .mid_top_with_margin_on(state.bg_ids.bg_frame, buttons_top)
905            .with_tooltip(self.tooltip_manager, txt, "", &bag_tooltip, TEXT_COLOR)
906            .set(state.ids.bag_details_btn, ui)
907            .was_clicked()
908        {
909            event = Some(Event::SetDetailsMode(!self.show.bag_details));
910        }
911        // Button to expand bag
912        let (txt, btn, hover, press) = if self.show.bag_inv {
913            (
914                "Show Loadout",
915                self.imgs.collapse_btn,
916                self.imgs.collapse_btn_hover,
917                self.imgs.collapse_btn_press,
918            )
919        } else {
920            (
921                "Expand Bag",
922                self.imgs.expand_btn,
923                self.imgs.expand_btn_hover,
924                self.imgs.expand_btn_press,
925            )
926        };
927        let expand_btn = Button::image(btn)
928            .w_h(30.0, 17.0)
929            .hover_image(hover)
930            .press_image(press);
931
932        // Only show expand button when it's needed...
933        if (inventory.slots().count() > 45 || self.show.bag_inv)
934            && expand_btn
935                .top_right_with_margins_on(state.bg_ids.bg_frame, buttons_top, 37.0)
936                .with_tooltip(self.tooltip_manager, txt, "", &bag_tooltip, TEXT_COLOR)
937                .set(state.ids.bag_expand_btn, ui)
938                .was_clicked()
939        {
940            event = Some(Event::BagExpand);
941        }
942
943        // Sort mode inventory button
944        if Button::image(self.imgs.inv_sort_btn)
945            .w_h(30.0, 17.0)
946            .hover_image(self.imgs.inv_sort_btn_hover)
947            .press_image(self.imgs.inv_sort_btn_press)
948            .top_left_with_margins_on(state.bg_ids.bg_frame, buttons_top, 87.0) // 30 + 10 + 47
949            .with_tooltip(
950                self.tooltip_manager,
951                &(match self.global_state.settings.inventory.sort_order.next() {
952                    InventorySortOrder::Name => i18n.get_msg("hud-bag-change_to_sort_by_name"),
953                    InventorySortOrder::Quality => i18n.get_msg("hud-bag-change_to_sort_by_quality"),
954                    InventorySortOrder::Category => i18n.get_msg("hud-bag-change_to_sort_by_category"),
955                    InventorySortOrder::Tag => i18n.get_msg("hud-bag-change_to_sort_by_tag"),
956                    InventorySortOrder::Amount => i18n.get_msg("hud-bag-change_to_sort_by_quantity"),
957                }),
958                "",
959                &tooltip,
960                color::WHITE,
961            )
962            .set(state.ids.inventory_sort, ui)
963            .was_clicked()
964        {
965            // cycle sorting mode
966            event = Some(Event::ChangeInventorySortOrder(
967                self.global_state.settings.inventory.sort_order.next(),
968            ));
969        }
970        // Sort inventory button with selected mode
971        if Button::image(self.imgs.inv_sort_selected_btn)
972            .w_h(30.0, 17.0)
973            .hover_image(self.imgs.inv_sort_selected_btn_hover)
974            .press_image(self.imgs.inv_sort_selected_btn_press)
975            .top_left_with_margins_on(state.bg_ids.bg_frame, buttons_top, 47.0)
976            .with_tooltip(
977                self.tooltip_manager,
978                &(match self.global_state.settings.inventory.sort_order {
979                    InventorySortOrder::Name => i18n.get_msg("hud-bag-sort_by_name"),
980                    InventorySortOrder::Quality => i18n.get_msg("hud-bag-sort_by_quality"),
981                    InventorySortOrder::Category => i18n.get_msg("hud-bag-sort_by_category"),
982                    InventorySortOrder::Tag => i18n.get_msg("hud-bag-sort_by_tag"),
983                    InventorySortOrder::Amount => i18n.get_msg("hud-bag-sort_by_quantity"),
984                }),
985                "",
986                &tooltip,
987                color::WHITE,
988            )
989            .set(state.ids.inventory_sort_selected, ui)
990            .was_clicked()
991        {
992            event = Some(Event::SortInventory(
993                self.global_state.settings.inventory.sort_order,
994            ));
995        }
996
997        // Armor Slots
998        let mut slot_maker = SlotMaker {
999            empty_slot: self.imgs.armor_slot_empty,
1000            filled_slot: self.imgs.armor_slot,
1001            selected_slot: self.imgs.armor_slot_sel,
1002            background_color: Some(UI_HIGHLIGHT_0),
1003            content_size: ContentSize {
1004                width_height_ratio: 1.0,
1005                max_fraction: 0.75, /* Changes the item image size by setting a maximum
1006                                     * fraction
1007                                     * of either the width or height */
1008            },
1009            selected_content_scale: 1.067,
1010            amount_font: self.fonts.cyri.conrod_id,
1011            amount_margins: Vec2::new(-4.0, 0.0),
1012            amount_font_size: self.fonts.cyri.scale(12),
1013            amount_text_color: TEXT_COLOR,
1014            content_source: inventory,
1015            image_source: self.item_imgs,
1016            slot_manager: Some(self.slot_manager),
1017            pulse: self.pulse,
1018        };
1019
1020        // NOTE: Yes, macros considered harmful.
1021        // Though, this code mutably captures two different fields of `self`
1022        // This works because it's different branches of if-let
1023        // so in reality borrow checker allows you to do this as you
1024        // capture only one field.
1025        //
1026        // The less impossible, but still tricky part is denote type of
1027        // `$slot_maker` which has 1 lifetype parameter and 3 type parameters
1028        // in such way that it implements all traits conrod needs.
1029        //
1030        // And final part is that this uses that much of arguments
1031        // that just by passing all of them, you will get about the same
1032        // amount of lines this macro has or even more.
1033        //
1034        // So considering how many times we copy-paste this code
1035        // and how easy this macro looks it sounds like lawful evil.
1036        //
1037        // What this actually does is checks if we have equipped item on this slot
1038        // and if we do, display item tooltip for it.
1039        // If not, just show text of slot name.
1040        macro_rules! set_tooltip {
1041            ($slot_maker:expr, $slot_id:expr, $slot:expr, $desc:expr) => {
1042                if let Some(item) = inventory.equipped($slot) {
1043                    let manager = &mut *self.item_tooltip_manager;
1044                    $slot_maker
1045                        .with_item_tooltip(
1046                            manager,
1047                            core::iter::once(item as &dyn ItemDesc),
1048                            &None,
1049                            &item_tooltip,
1050                        )
1051                        .set($slot_id, ui)
1052                } else {
1053                    let manager = &mut *self.tooltip_manager;
1054                    $slot_maker
1055                        .with_tooltip(manager, &i18n.get_msg($desc), "", &tooltip, color::WHITE)
1056                        .set($slot_id, ui)
1057                }
1058            };
1059        }
1060
1061        let filled_slot = self.imgs.armor_slot;
1062        if !self.show.bag_inv {
1063            // Stat icons and text
1064            state.update(|s| {
1065                s.ids
1066                    .stat_icons
1067                    .resize(STATS.len(), &mut ui.widget_id_generator())
1068            });
1069            state.update(|s| {
1070                s.ids
1071                    .stat_txts
1072                    .resize(STATS.len(), &mut ui.widget_id_generator())
1073            });
1074            // Stats
1075            let combat_rating = combat_rating(
1076                inventory,
1077                self.health,
1078                self.energy,
1079                self.poise,
1080                self.skill_set,
1081                *self.body,
1082                self.msm,
1083            )
1084            .min(999.9);
1085            let indicator_col = cr_color(combat_rating);
1086            for i in STATS.iter().copied().enumerate() {
1087                let btn = Button::image(match i.1 {
1088                    "Health" => self.imgs.health_ico,
1089                    "Energy" => self.imgs.energy_ico,
1090                    "Combat Rating" => self.imgs.combat_rating_ico,
1091                    "Protection" => self.imgs.protection_ico,
1092                    "Stun Resilience" => self.imgs.stun_res_ico,
1093                    "Stealth" => self.imgs.stealth_rating_ico,
1094                    _ => self.imgs.nothing,
1095                })
1096                .w_h(20.0, 20.0)
1097                .image_color(if i.1 == "Combat Rating" {
1098                    indicator_col
1099                } else {
1100                    TEXT_COLOR
1101                });
1102                let protection_txt = format!(
1103                    "{}%",
1104                    (100.0
1105                        * Damage::compute_damage_reduction(
1106                            None,
1107                            Some(inventory),
1108                            Some(self.stats),
1109                            self.msm
1110                        )) as i32
1111                );
1112                let health_txt = format!("{}", self.health.maximum().round() as usize);
1113                let energy_txt = format!("{}", self.energy.maximum().round() as usize);
1114                let combat_rating_txt = format!("{}", (combat_rating * 10.0) as usize);
1115                let stun_res_txt = format!(
1116                    "{}",
1117                    (100.0
1118                        * Poise::compute_poise_damage_reduction(
1119                            Some(inventory),
1120                            self.msm,
1121                            None,
1122                            Some(self.stats),
1123                        )) as i32
1124                );
1125                let stealth_txt = format!(
1126                    "{:.1}%",
1127                    ((1.0
1128                        - perception_dist_multiplier_from_stealth(
1129                            Some(inventory),
1130                            None,
1131                            self.msm
1132                        ))
1133                        * 100.0)
1134                );
1135                let btn = if i.0 == 0 {
1136                    btn.top_left_with_margins_on(state.bg_ids.bg_frame, 55.0, 10.0)
1137                } else {
1138                    btn.down_from(state.ids.stat_icons[i.0 - 1], 7.0)
1139                };
1140                let tooltip_head = match i.1 {
1141                    "Health" => i18n.get_msg("hud-bag-health"),
1142                    "Energy" => i18n.get_msg("hud-bag-energy"),
1143                    "Combat Rating" => i18n.get_msg("hud-bag-combat_rating"),
1144                    "Protection" => i18n.get_msg("hud-bag-protection"),
1145                    "Stun Resilience" => i18n.get_msg("hud-bag-stun_res"),
1146                    "Stealth" => i18n.get_msg("hud-bag-stealth"),
1147                    _ => Cow::Borrowed(""),
1148                };
1149                let tooltip_txt = match i.1 {
1150                    "Combat Rating" => i18n.get_msg("hud-bag-combat_rating_desc"),
1151                    "Protection" => i18n.get_msg("hud-bag-protection_desc"),
1152                    "Stun Resilience" => i18n.get_msg("hud-bag-stun_res_desc"),
1153                    _ => Cow::Borrowed(""),
1154                };
1155                btn.with_tooltip(
1156                    self.tooltip_manager,
1157                    &tooltip_head,
1158                    &tooltip_txt,
1159                    &bag_tooltip,
1160                    TEXT_COLOR,
1161                )
1162                .set(state.ids.stat_icons[i.0], ui);
1163                Text::new(match i.1 {
1164                    "Health" => &health_txt,
1165                    "Energy" => &energy_txt,
1166                    "Combat Rating" => &combat_rating_txt,
1167                    "Protection" => &protection_txt,
1168                    "Stun Resilience" => &stun_res_txt,
1169                    "Stealth" => &stealth_txt,
1170                    _ => "",
1171                })
1172                .right_from(state.ids.stat_icons[i.0], 10.0)
1173                .font_id(self.fonts.cyri.conrod_id)
1174                .font_size(self.fonts.cyri.scale(14))
1175                .color(TEXT_COLOR)
1176                .graphics_for(state.ids.stat_icons[i.0])
1177                .set(state.ids.stat_txts[i.0], ui);
1178            }
1179            // Loadout Slots
1180            //  Head
1181            let item_slot = EquipSlot::Armor(ArmorSlot::Head);
1182            let slot = slot_maker
1183                .fabricate(item_slot, [45.0; 2])
1184                .mid_top_with_margin_on(state.bg_ids.bg_frame, 60.0)
1185                .with_icon(self.imgs.head_bg, Vec2::new(32.0, 40.0), Some(UI_MAIN))
1186                .filled_slot(filled_slot);
1187
1188            let slot_id = state.ids.head_slot;
1189            set_tooltip!(slot, slot_id, item_slot, "hud-bag-head");
1190
1191            //  Necklace
1192            let item_slot = EquipSlot::Armor(ArmorSlot::Neck);
1193            let slot = slot_maker
1194                .fabricate(item_slot, [45.0; 2])
1195                .mid_bottom_with_margin_on(state.ids.head_slot, -55.0)
1196                .with_icon(self.imgs.necklace_bg, Vec2::new(40.0, 31.0), Some(UI_MAIN))
1197                .filled_slot(filled_slot);
1198
1199            let slot_id = state.ids.neck_slot;
1200            set_tooltip!(slot, slot_id, item_slot, "hud-bag-neck");
1201
1202            // Chest
1203            //Image::new(self.imgs.armor_slot) // different graphics for empty/non empty
1204            let item_slot = EquipSlot::Armor(ArmorSlot::Chest);
1205            let slot = slot_maker
1206                .fabricate(item_slot, [85.0; 2])
1207                .mid_bottom_with_margin_on(state.ids.neck_slot, -95.0)
1208                .with_icon(self.imgs.chest_bg, Vec2::new(64.0, 42.0), Some(UI_MAIN))
1209                .filled_slot(filled_slot);
1210
1211            let slot_id = state.ids.chest_slot;
1212            set_tooltip!(slot, slot_id, item_slot, "hud-bag-chest");
1213
1214            //  Shoulders
1215            let item_slot = EquipSlot::Armor(ArmorSlot::Shoulders);
1216            let slot = slot_maker
1217                .fabricate(item_slot, [70.0; 2])
1218                .bottom_left_with_margins_on(state.ids.chest_slot, 0.0, -80.0)
1219                .with_icon(self.imgs.shoulders_bg, Vec2::new(60.0, 36.0), Some(UI_MAIN))
1220                .filled_slot(filled_slot);
1221
1222            let slot_id = state.ids.shoulders_slot;
1223            set_tooltip!(slot, slot_id, item_slot, "hud-bag-shoulders");
1224
1225            // Hands
1226            let item_slot = EquipSlot::Armor(ArmorSlot::Hands);
1227            let slot = slot_maker
1228                .fabricate(item_slot, [70.0; 2])
1229                .bottom_right_with_margins_on(state.ids.chest_slot, 0.0, -80.0)
1230                .with_icon(self.imgs.hands_bg, Vec2::new(55.0, 60.0), Some(UI_MAIN))
1231                .filled_slot(filled_slot);
1232
1233            let slot_id = state.ids.hands_slot;
1234            set_tooltip!(slot, slot_id, item_slot, "hud-bag-hands");
1235
1236            // Belt
1237            let item_slot = EquipSlot::Armor(ArmorSlot::Belt);
1238            let slot = slot_maker
1239                .fabricate(item_slot, [45.0; 2])
1240                .mid_bottom_with_margin_on(state.ids.chest_slot, -55.0)
1241                .with_icon(self.imgs.belt_bg, Vec2::new(40.0, 23.0), Some(UI_MAIN))
1242                .filled_slot(filled_slot);
1243
1244            let slot_id = state.ids.belt_slot;
1245            set_tooltip!(slot, slot_id, item_slot, "hud-bag-belt");
1246
1247            // Legs
1248            let item_slot = EquipSlot::Armor(ArmorSlot::Legs);
1249            let slot = slot_maker
1250                .fabricate(item_slot, [85.0; 2])
1251                .mid_bottom_with_margin_on(state.ids.belt_slot, -95.0)
1252                .with_icon(self.imgs.legs_bg, Vec2::new(48.0, 70.0), Some(UI_MAIN))
1253                .filled_slot(filled_slot);
1254
1255            let slot_id = state.ids.legs_slot;
1256            set_tooltip!(slot, slot_id, item_slot, "hud-bag-legs");
1257
1258            // Ring
1259            let item_slot = EquipSlot::Armor(ArmorSlot::Ring1);
1260            let slot = slot_maker
1261                .fabricate(item_slot, [45.0; 2])
1262                .bottom_left_with_margins_on(state.ids.hands_slot, -55.0, 0.0)
1263                .with_icon(self.imgs.ring_bg, Vec2::new(36.0, 40.0), Some(UI_MAIN))
1264                .filled_slot(filled_slot);
1265
1266            let slot_id = state.ids.ring1_slot;
1267            set_tooltip!(slot, slot_id, item_slot, "hud-bag-ring");
1268
1269            // Ring 2
1270            let item_slot = EquipSlot::Armor(ArmorSlot::Ring2);
1271            let slot = slot_maker
1272                .fabricate(item_slot, [45.0; 2])
1273                .bottom_right_with_margins_on(state.ids.shoulders_slot, -55.0, 0.0)
1274                .with_icon(self.imgs.ring_bg, Vec2::new(36.0, 40.0), Some(UI_MAIN))
1275                .filled_slot(filled_slot);
1276
1277            let slot_id = state.ids.ring2_slot;
1278            set_tooltip!(slot, slot_id, item_slot, "hud-bag-ring");
1279
1280            // Back
1281            let item_slot = EquipSlot::Armor(ArmorSlot::Back);
1282            let slot = slot_maker
1283                .fabricate(item_slot, [45.0; 2])
1284                .down_from(state.ids.ring2_slot, 10.0)
1285                .with_icon(self.imgs.back_bg, Vec2::new(33.0, 40.0), Some(UI_MAIN))
1286                .filled_slot(filled_slot);
1287
1288            let slot_id = state.ids.back_slot;
1289            set_tooltip!(slot, slot_id, item_slot, "hud-bag-back");
1290
1291            // Foot
1292            let item_slot = EquipSlot::Armor(ArmorSlot::Feet);
1293            let slot = slot_maker
1294                .fabricate(item_slot, [45.0; 2])
1295                .down_from(state.ids.ring1_slot, 10.0)
1296                .with_icon(self.imgs.feet_bg, Vec2::new(32.0, 40.0), Some(UI_MAIN))
1297                .filled_slot(filled_slot);
1298
1299            let slot_id = state.ids.feet_slot;
1300            set_tooltip!(slot, slot_id, item_slot, "hud-bag-feet");
1301
1302            // Lantern
1303            let item_slot = EquipSlot::Lantern;
1304            let slot = slot_maker
1305                .fabricate(item_slot, [45.0; 2])
1306                .top_right_with_margins_on(state.bg_ids.bg_frame, 60.0, 5.0)
1307                .with_icon(self.imgs.lantern_bg, Vec2::new(24.0, 38.0), Some(UI_MAIN))
1308                .filled_slot(filled_slot);
1309
1310            let slot_id = state.ids.lantern_slot;
1311            set_tooltip!(slot, slot_id, item_slot, "hud-bag-lantern");
1312
1313            // Glider
1314            let item_slot = EquipSlot::Glider;
1315            let slot = slot_maker
1316                .fabricate(item_slot, [45.0; 2])
1317                .down_from(state.ids.lantern_slot, 5.0)
1318                .with_icon(self.imgs.glider_bg, Vec2::new(38.0, 38.0), Some(UI_MAIN))
1319                .filled_slot(filled_slot);
1320
1321            let slot_id = state.ids.glider_slot;
1322            set_tooltip!(slot, slot_id, item_slot, "hud-bag-glider");
1323
1324            // Tabard
1325            let item_slot = EquipSlot::Armor(ArmorSlot::Tabard);
1326            let slot = slot_maker
1327                .fabricate(item_slot, [45.0; 2])
1328                .down_from(state.ids.glider_slot, 5.0)
1329                .with_icon(self.imgs.tabard_bg, Vec2::new(38.0, 38.0), Some(UI_MAIN))
1330                .filled_slot(filled_slot);
1331
1332            let slot_id = state.ids.tabard_slot;
1333            set_tooltip!(slot, slot_id, item_slot, "hud-bag-tabard");
1334
1335            // Active Mainhand/Left-Slot
1336            let item_slot = EquipSlot::ActiveMainhand;
1337            let slot = slot_maker
1338                .fabricate(item_slot, [85.0; 2])
1339                .bottom_right_with_margins_on(state.ids.back_slot, -95.0, 0.0)
1340                .with_icon(self.imgs.mainhand_bg, Vec2::new(75.0, 75.0), Some(UI_MAIN))
1341                .filled_slot(filled_slot);
1342
1343            let slot_id = state.ids.active_mainhand_slot;
1344            set_tooltip!(slot, slot_id, item_slot, "hud-bag-mainhand");
1345
1346            // Active Offhand/Right-Slot
1347            let item_slot = EquipSlot::ActiveOffhand;
1348            let slot = slot_maker
1349                .fabricate(item_slot, [85.0; 2])
1350                .bottom_left_with_margins_on(state.ids.feet_slot, -95.0, 0.0)
1351                .with_icon(self.imgs.offhand_bg, Vec2::new(75.0, 75.0), Some(UI_MAIN))
1352                .filled_slot(filled_slot);
1353
1354            let slot_id = state.ids.active_offhand_slot;
1355            set_tooltip!(slot, slot_id, item_slot, "hud-bag-offhand");
1356
1357            // Inactive Mainhand/Left-Slot
1358            let item_slot = EquipSlot::InactiveMainhand;
1359            let slot = slot_maker
1360                .fabricate(item_slot, [40.0; 2])
1361                .bottom_right_with_margins_on(state.ids.active_mainhand_slot, 3.0, -47.0)
1362                .with_icon(self.imgs.mainhand_bg, Vec2::new(35.0, 35.0), Some(UI_MAIN))
1363                .filled_slot(filled_slot);
1364
1365            let slot_id = state.ids.inactive_mainhand_slot;
1366            set_tooltip!(slot, slot_id, item_slot, "hud-bag-inactive_mainhand");
1367
1368            // Inactive Offhand/Right-Slot
1369            let item_slot = EquipSlot::InactiveOffhand;
1370            let slot = slot_maker
1371                .fabricate(item_slot, [40.0; 2])
1372                .bottom_left_with_margins_on(state.ids.active_offhand_slot, 3.0, -47.0)
1373                .with_icon(self.imgs.offhand_bg, Vec2::new(35.0, 35.0), Some(UI_MAIN))
1374                .filled_slot(filled_slot);
1375
1376            let slot_id = state.ids.inactive_offhand_slot;
1377            set_tooltip!(slot, slot_id, item_slot, "hud-bag-inactive_offhand");
1378
1379            if Button::image(self.imgs.swap_equipped_weapons_btn)
1380                .hover_image(self.imgs.swap_equipped_weapons_btn_hover)
1381                .press_image(self.imgs.swap_equipped_weapons_btn_press)
1382                .w_h(32.0, 40.0)
1383                .bottom_left_with_margins_on(state.bg_ids.bg_frame, 0.0, 23.3)
1384                .align_middle_y_of(state.ids.active_mainhand_slot)
1385                .with_tooltip(
1386                    self.tooltip_manager,
1387                    &i18n.get_msg("hud-bag-swap_equipped_weapons_title"),
1388                    &(if let Some(key) = self
1389                        .global_state
1390                        .settings
1391                        .controls
1392                        .get_binding(GameInput::SwapLoadout)
1393                    {
1394                        i18n.get_msg_ctx(
1395                            "hud-bag-swap_equipped_weapons_desc",
1396                            &i18n::fluent_args! {
1397                                "key" => key.display_string()
1398                            },
1399                        )
1400                    } else {
1401                        Cow::Borrowed("")
1402                    }),
1403                    &tooltip,
1404                    color::WHITE,
1405                )
1406                .set(state.ids.swap_equipped_weapons_btn, ui)
1407                .was_clicked()
1408            {
1409                event = Some(Event::SwapEquippedWeapons);
1410            }
1411        }
1412
1413        // Bag 1
1414        let item_slot = EquipSlot::Armor(ArmorSlot::Bag1);
1415        let slot = slot_maker
1416            .fabricate(item_slot, [35.0; 2])
1417            .bottom_left_with_margins_on(
1418                state.bg_ids.bg_frame,
1419                if self.show.bag_inv { 600.0 } else { 167.0 },
1420                3.0,
1421            )
1422            .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN))
1423            .filled_slot(filled_slot);
1424
1425        let slot_id = state.ids.bag1_slot;
1426        set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
1427
1428        // Bag 2
1429        let item_slot = EquipSlot::Armor(ArmorSlot::Bag2);
1430        let slot = slot_maker
1431            .fabricate(item_slot, [35.0; 2])
1432            .down_from(state.ids.bag1_slot, 2.0)
1433            .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN))
1434            .filled_slot(filled_slot);
1435
1436        let slot_id = state.ids.bag2_slot;
1437        set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
1438
1439        // Bag 3
1440        let item_slot = EquipSlot::Armor(ArmorSlot::Bag3);
1441        let slot = slot_maker
1442            .fabricate(item_slot, [35.0; 2])
1443            .down_from(state.ids.bag2_slot, 2.0)
1444            .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN))
1445            .filled_slot(filled_slot);
1446
1447        let slot_id = state.ids.bag3_slot;
1448        set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
1449
1450        // Bag 4
1451        let item_slot = EquipSlot::Armor(ArmorSlot::Bag4);
1452        let slot = slot_maker
1453            .fabricate(item_slot, [35.0; 2])
1454            .down_from(state.ids.bag3_slot, 2.0)
1455            .with_icon(self.imgs.bag_bg, Vec2::new(28.0, 24.0), Some(UI_MAIN))
1456            .filled_slot(filled_slot);
1457
1458        let slot_id = state.ids.bag4_slot;
1459        set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
1460
1461        // Close button
1462        if Button::image(self.imgs.close_btn)
1463            .w_h(24.0, 25.0)
1464            .hover_image(self.imgs.close_btn_hover)
1465            .press_image(self.imgs.close_btn_press)
1466            .top_right_with_margins_on(state.bg_ids.bg, 0.0, 0.0)
1467            .set(state.ids.bag_close, ui)
1468            .was_clicked()
1469        {
1470            event = Some(Event::Close);
1471        }
1472        event
1473    }
1474}