veloren_voxygen/hud/
bag.rs

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