Skip to main content

veloren_voxygen/hud/
bag.rs

1use super::{
2    CRITICAL_HP_COLOR, HudInfo, LOW_HP_COLOR, Show, SlotGrid, TEXT_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
3    cr_color,
4    img_ids::{Imgs, ImgsRot},
5    item_imgs::ItemImgs,
6    slot_grid::SlotEvents,
7    slots::{ArmorSlot, EquipSlot, SlotManager},
8};
9use crate::{
10    GlobalState,
11    game_input::GameInput,
12    hud::{controller_icons as icon_utils, slot_grid::TabFilters},
13    settings::{
14        HudPositionSettings,
15        hud_position::{
16            DEFAULT_OTHER_BAG_HEIGHT, DEFAULT_OTHER_BAG_WIDTH, DEFAULT_OWN_BAG_HEIGHT,
17            DEFAULT_OWN_BAG_WIDTH,
18        },
19    },
20    ui::{
21        ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable, RichText, Tooltip,
22        TooltipManager, Tooltipable,
23        fonts::Fonts,
24        slot::{ContentSize, SlotMaker},
25    },
26    window::{LastInput, MenuInput},
27};
28use client::Client;
29use common::{
30    combat::{Damage, combat_rating, perception_dist_multiplier_from_stealth},
31    comp::{
32        Body, Energy, Health, Inventory, Poise, SkillSet, Stats,
33        inventory::InventorySortOrder,
34        item::{ItemDesc, ItemI18n, MaterialStatManifest},
35    },
36    recipe::RecipeBookManifest,
37};
38use conrod_core::{
39    Color, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, builder_methods, color,
40    widget::{self, Button, Image, Rectangle, Scrollbar, State as ConrodState, Text},
41    widget_ids,
42};
43use i18n::Localization;
44use std::borrow::Cow;
45
46use specs::Entity as EcsEntity;
47use vek::{Vec2, approx::AbsDiffEq};
48
49const STATS: [&str; 6] = [
50    "Health",
51    "Energy",
52    "Protection",
53    "Combat Rating",
54    "Stun Resilience",
55    "Stealth",
56];
57
58widget_ids! {
59    pub struct InventoryScrollerIds {
60        draggable_area,
61        inv_alignment,
62        spacing_above,
63        slot_grid,
64        //coin_ico,
65        space_txt,
66        //coin_txt,
67        inventory_title,
68        inventory_title_bg,
69        scrollbar_bg,
70        second_phase_scrollbar_bg,
71        scrollbar_slots,
72        left_scrollbar_slots,
73    }
74}
75
76pub struct InventoryScrollerState {
77    ids: InventoryScrollerIds,
78}
79
80pub enum InventoryScrollerEvent {
81    Drag(Vec2<f64>),
82    Close,
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    menu_events: &'a Vec<MenuInput>,
97    active_content: usize,
98    localized_strings: &'a Localization,
99    item_i18n: &'a ItemI18n,
100    show_stats: bool,
101    show_bag_inv: bool,
102    on_right: bool,
103    global_state: &'a GlobalState,
104    item_tooltip: &'a ItemTooltip<'a>,
105    playername: String,
106    entity: EcsEntity,
107    is_us: bool,
108    inventory: &'a Inventory,
109    bg_ids: &'a BackgroundIds,
110    show_salvage: bool,
111    details_mode: bool,
112}
113
114impl<'a> InventoryScroller<'a> {
115    // InventoryScroller is only used by the trade screen now, and should be removed
116    // sometime TODO: replace this with the new bag defined further below
117    #[expect(clippy::too_many_arguments)]
118    pub fn new(
119        client: &'a Client,
120        global_state: &'a GlobalState,
121        imgs: &'a Imgs,
122        item_imgs: &'a ItemImgs,
123        fonts: &'a Fonts,
124        item_tooltip_manager: &'a mut ItemTooltipManager,
125        slot_manager: &'a mut SlotManager,
126        pulse: f32,
127        menu_events: &'a Vec<MenuInput>,
128        active_content: usize,
129        localized_strings: &'a Localization,
130        item_i18n: &'a ItemI18n,
131        show_stats: bool,
132        show_bag_inv: bool,
133        on_right: bool,
134        item_tooltip: &'a ItemTooltip<'a>,
135        playername: String,
136        entity: EcsEntity,
137        is_us: bool,
138        inventory: &'a Inventory,
139        bg_ids: &'a BackgroundIds,
140        show_salvage: bool,
141        details_mode: bool,
142    ) -> Self {
143        InventoryScroller {
144            client,
145            imgs,
146            item_imgs,
147            fonts,
148            common: widget::CommonBuilder::default(),
149            item_tooltip_manager,
150            slot_manager,
151            pulse,
152            menu_events,
153            active_content,
154            localized_strings,
155            item_i18n,
156            show_stats,
157            show_bag_inv,
158            on_right,
159            global_state,
160            item_tooltip,
161            playername,
162            entity,
163            is_us,
164            inventory,
165            bg_ids,
166            show_salvage,
167            details_mode,
168        }
169    }
170
171    fn background(&mut self, ui: &mut UiCell<'_>) {
172        let bag_settings = &self.global_state.settings.hud_position;
173        let bag_pos = if !self.on_right {
174            bag_settings.bag.other
175        } else {
176            bag_settings.bag.own
177        };
178
179        let bg_id = if !self.on_right {
180            self.imgs.inv_bg_bag
181        } else {
182            self.imgs.player_inv_bg_bag
183        };
184
185        let img_id = if !self.on_right {
186            self.imgs.inv_frame_bag
187        } else {
188            self.imgs.player_inv_frame_bag
189        };
190
191        let mut bg = Image::new(if self.show_stats {
192            self.imgs.inv_bg_stats
193        } else if self.show_bag_inv {
194            bg_id
195        } else {
196            self.imgs.inv_bg_armor
197        })
198        .w_h(
199            424.0,
200            if self.show_bag_inv && !self.on_right {
201                548.0
202            } else {
203                708.0
204            },
205        );
206
207        if self.on_right {
208            bg = bg.bottom_right_with_margins_on(ui.window, bag_pos.y, bag_pos.x);
209        } else {
210            bg = bg.bottom_left_with_margins_on(ui.window, bag_pos.y, bag_pos.x);
211        }
212
213        bg.color(Some(UI_MAIN)).set(self.bg_ids.bg, ui);
214
215        Image::new(if self.show_bag_inv {
216            img_id
217        } else {
218            self.imgs.inv_frame
219        })
220        .w_h(
221            424.0,
222            if self.show_bag_inv && !self.on_right {
223                548.0
224            } else {
225                708.0
226            },
227        )
228        .middle_of(self.bg_ids.bg)
229        .color(Some(UI_HIGHLIGHT_0))
230        .set(self.bg_ids.bg_frame, ui);
231    }
232
233    fn title(&mut self, state: &ConrodState<'_, InventoryScrollerState>, ui: &mut UiCell<'_>) {
234        Text::new(
235            &self
236                .localized_strings
237                .get_msg_ctx("hud-bag-inventory", &i18n::fluent_args! {
238                    "playername" => &*self.playername,
239                }),
240        )
241        .mid_top_with_margin_on(self.bg_ids.bg_frame, 9.0)
242        .font_id(self.fonts.cyri.conrod_id)
243        .font_size(self.fonts.cyri.scale(22))
244        .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
245        .set(state.ids.inventory_title_bg, ui);
246        Text::new(
247            &self
248                .localized_strings
249                .get_msg_ctx("hud-bag-inventory", &i18n::fluent_args! {
250                    "playername" => &*self.playername,
251                }),
252        )
253        .top_left_with_margins_on(state.ids.inventory_title_bg, 2.0, 2.0)
254        .font_id(self.fonts.cyri.conrod_id)
255        .font_size(self.fonts.cyri.scale(22))
256        .color(TEXT_COLOR)
257        .set(state.ids.inventory_title, ui);
258    }
259
260    fn scrollbar_and_slots(
261        &mut self,
262        state: &mut ConrodState<'_, InventoryScrollerState>,
263        events: &mut Vec<InventoryScrollerEvent>,
264        ui: &mut UiCell<'_>,
265    ) {
266        // MENU INPUTS: change the inventory button/filter focus
267        // Back: creates a close event
268        if self.active_content == 1 {
269            for event in self.menu_events {
270                match *event {
271                    MenuInput::Apply => {
272                        // TODO
273                    },
274                    MenuInput::Back => {
275                        events.push(InventoryScrollerEvent::Close);
276                    },
277                    _ => {},
278                }
279            }
280        }
281
282        let grid_width = 376.0;
283        let grid_height = if self.show_bag_inv && !self.on_right {
284            450.0 // This for the left bag
285        } else if self.show_bag_inv && self.on_right {
286            600.0 // This for the expanded right bag
287        } else {
288            200.0
289        };
290
291        // Alignment for Grid
292        Rectangle::fill_with([grid_width, grid_height], color::TRANSPARENT)
293            .mid_bottom_with_margin_on(self.bg_ids.bg_frame, 21.0)
294            .scroll_kids_vertically()
295            .set(state.ids.inv_alignment, ui);
296
297        // Adds spacing above the slots to make scrolling feel a bit more natural
298        Rectangle::fill_with([grid_width, 5.0], color::TRANSPARENT)
299            .mid_top_of(state.ids.inv_alignment)
300            .set(state.ids.spacing_above, ui);
301
302        let mut space_max = 0;
303        let compact_mode = self
304            .global_state
305            .settings
306            .interface
307            .toggle_compact_item_slots;
308        let columns = if compact_mode { 9 } else { 6 };
309        let spacing = if compact_mode { 2.0 } else { 5.8 };
310        let slot_size = if compact_mode { 40.0 } else { 57.8 };
311
312        // Bag Slots
313        for event in SlotGrid::new(
314            self.client,
315            self.global_state,
316            self.imgs,
317            self.item_imgs,
318            self.fonts,
319            self.item_tooltip_manager,
320            self.slot_manager,
321            self.inventory,
322            self.item_tooltip,
323            self.localized_strings,
324            self.item_i18n,
325            self.entity,
326            self.pulse,
327            self.menu_events,
328            self.is_us,
329            self.details_mode,
330            self.show_salvage,
331        )
332        .columns(columns)
333        .spacing(if self.details_mode { 0.0 } else { spacing })
334        .slot_size(if self.details_mode { 20.0 } else { slot_size })
335        .w_of(state.ids.inv_alignment)
336        .down_from(state.ids.spacing_above, 0.0)
337        .set(state.ids.slot_grid, ui)
338        {
339            match event {
340                SlotEvents::Close => {
341                    events.push(InventoryScrollerEvent::Close);
342                },
343                SlotEvents::FilteredSize(size) => {
344                    space_max = size;
345                },
346                _ => {},
347            }
348        }
349
350        // Slots Scrollbar
351        if space_max > 45 && !self.show_bag_inv {
352            // Scrollbar-BG
353            Image::new(self.imgs.scrollbar_bg)
354                .w_h(9.0, 173.0)
355                .bottom_right_with_margins_on(self.bg_ids.bg_frame, 42.0, 3.0)
356                .color(Some(UI_HIGHLIGHT_0))
357                .set(state.ids.scrollbar_bg, ui);
358            // Scrollbar
359            Scrollbar::y_axis(state.ids.inv_alignment)
360                .thickness(5.0)
361                .h(123.0)
362                .color(UI_MAIN)
363                .middle_of(state.ids.scrollbar_bg)
364                .set(state.ids.scrollbar_slots, ui);
365        } else if space_max > 135 && self.on_right {
366            // Scrollbar-BG
367            Image::new(self.imgs.scrollbar_bg_big)
368                .w_h(9.0, 592.0)
369                .bottom_right_with_margins_on(self.bg_ids.bg_frame, 42.0, 3.0)
370                .color(Some(UI_HIGHLIGHT_0))
371                .set(state.ids.scrollbar_bg, ui);
372            // Scrollbar
373            Scrollbar::y_axis(state.ids.inv_alignment)
374                .thickness(5.0)
375                .h(542.0)
376                .color(UI_MAIN)
377                .middle_of(state.ids.scrollbar_bg)
378                .set(state.ids.scrollbar_slots, ui);
379        };
380
381        // This is just for the offeror inventory scrollbar
382        if space_max >= 108 && !self.on_right && self.show_bag_inv {
383            // Left bag scrollbar background
384            Image::new(self.imgs.second_phase_scrollbar_bg)
385                .w_h(9.0, 434.0)
386                .bottom_right_with_margins_on(self.bg_ids.bg_frame, 42.0, 3.0)
387                .color(Some(UI_HIGHLIGHT_0))
388                .set(state.ids.second_phase_scrollbar_bg, ui);
389            // Left bag scrollbar
390            Scrollbar::y_axis(state.ids.inv_alignment)
391                .thickness(5.0)
392                .h(384.0)
393                .color(UI_MAIN)
394                .middle_of(state.ids.second_phase_scrollbar_bg)
395                .set(state.ids.left_scrollbar_slots, ui);
396        }
397    }
398
399    fn footer_metrics(
400        &mut self,
401        state: &ConrodState<'_, InventoryScrollerState>,
402        ui: &mut UiCell<'_>,
403    ) {
404        let space_used = self.inventory.populated_slots();
405        let space_max = self.inventory.slots().count();
406        let bag_space = format!("{}/{}", space_used, space_max);
407        let bag_space_percentage = space_used as f32 / space_max as f32;
408        //let coin_itemdef =
409        // Arc::<ItemDef>::load_expect_cloned("common.items.utility.coins"); let
410        // coin_count = self.inventory.item_count(&coin_itemdef); TODO: Reuse
411        // this to generally count a stackable item the player selected
412        // let cheese_itemdef =
413        // Arc::<ItemDef>::load_expect_cloned("common.items.food.cheese");
414        // let cheese_count = self.inventory.item_count(&cheese_itemdef);
415
416        // Coin Icon and Coin Text
417        /*Image::new(self.imgs.coin_ico)
418            .w_h(16.0, 17.0)
419            .bottom_left_with_margins_on(self.bg_ids.bg_frame, 2.0, 43.0)
420            .set(state.ids.coin_ico, ui);
421        Text::new(&format!("{}", coin_count))
422            .bottom_left_with_margins_on(self.bg_ids.bg_frame, 6.0, 64.0)
423            .font_id(self.fonts.cyri.conrod_id)
424            .font_size(self.fonts.cyri.scale(14))
425            .color(Color::Rgba(0.871, 0.863, 0.05, 1.0))
426            .set(state.ids.coin_txt, ui);*/
427        // TODO: Add a customizable counter for stackable items here
428        // TODO: Cheese is funny until it's real
429        /*Image::new(self.imgs.cheese_ico)
430            .w_h(16.0, 17.0)
431            .bottom_left_with_margins_on(self.bg_ids.bg_frame, 2.0, 110.0)
432            .set(state.ids.cheese_ico, ui);
433        Text::new(&format!("{}", cheese_count))
434            .bottom_left_with_margins_on(self.bg_ids.bg_frame, 6.0, 144.0)
435            .font_id(self.fonts.cyri.conrod_id)
436            .font_size(self.fonts.cyri.scale(14))
437            .color(Color::Rgba(0.871, 0.863, 0.05, 1.0))
438            .set(state.ids.cheese_txt, ui);*/
439        //Free Bag-Space
440        Text::new(&bag_space)
441            .bottom_right_with_margins_on(self.bg_ids.bg_frame, 6.0, 43.0)
442            .font_id(self.fonts.cyri.conrod_id)
443            .font_size(self.fonts.cyri.scale(14))
444            .color(if bag_space_percentage < 0.8 {
445                TEXT_COLOR
446            } else if bag_space_percentage < 1.0 {
447                LOW_HP_COLOR
448            } else {
449                CRITICAL_HP_COLOR
450            })
451            .set(state.ids.space_txt, ui);
452    }
453
454    fn draggable_area(
455        &self,
456        state: &ConrodState<'_, InventoryScrollerState>,
457        events: &mut Vec<InventoryScrollerEvent>,
458        ui: &mut UiCell<'_>,
459    ) {
460        let bag_settings = &self.global_state.settings.hud_position;
461        let bag_pos = if !self.on_right {
462            bag_settings.bag.other
463        } else {
464            bag_settings.bag.own
465        };
466
467        let bag_size: Vec2<f64> = if !self.on_right {
468            [DEFAULT_OTHER_BAG_WIDTH, DEFAULT_OTHER_BAG_HEIGHT].into()
469        } else {
470            [DEFAULT_OWN_BAG_WIDTH, DEFAULT_OWN_BAG_HEIGHT].into()
471        };
472
473        let pos_delta: Vec2<f64> = ui
474            .widget_input(state.ids.draggable_area)
475            .drags()
476            .left()
477            .map(|drag| Vec2::<f64>::from(drag.delta_xy))
478            .sum();
479
480        let pos_delta: Vec2<f64> = if !self.on_right {
481            // Others (left side) bags use bottom_left_with_margins_on
482            pos_delta
483        } else {
484            // Own (right side) bags use bottom_right_with_margins_on
485            // which means we have to use positive margins to move left
486            // so we have to invert the x value from the delta.
487            pos_delta.with_x(-pos_delta.x)
488        };
489
490        let window_clamp = Vec2::new(ui.win_w, ui.win_h) - bag_size;
491
492        let new_pos = (bag_pos + pos_delta)
493            .map(|e| e.max(0.))
494            .map2(window_clamp, |e, bounds| e.min(bounds));
495
496        if new_pos.abs_diff_ne(&bag_pos, f64::EPSILON) {
497            events.push(InventoryScrollerEvent::Drag(new_pos));
498        }
499
500        if ui
501            .widget_input(state.ids.draggable_area)
502            .clicks()
503            .right()
504            .count()
505            == 1
506        {
507            events.push(InventoryScrollerEvent::Drag(if self.on_right {
508                HudPositionSettings::default().bag.own
509            } else {
510                HudPositionSettings::default().bag.other
511            }));
512        }
513
514        Rectangle::fill_with([424.0, 48.0], color::TRANSPARENT)
515            .top_left_with_margin_on(self.bg_ids.bg_frame, 0.0)
516            .set(state.ids.draggable_area, ui);
517    }
518}
519
520impl Widget for InventoryScroller<'_> {
521    type Event = Vec<InventoryScrollerEvent>;
522    type State = InventoryScrollerState;
523    type Style = ();
524
525    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
526        InventoryScrollerState {
527            ids: InventoryScrollerIds::new(id_gen),
528        }
529    }
530
531    fn style(&self) -> Self::Style {}
532
533    fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
534        let widget::UpdateArgs { state, ui, .. } = args;
535        let mut events = Vec::new();
536        self.background(ui);
537        self.title(state, ui);
538        self.scrollbar_and_slots(state, &mut events, ui);
539        self.footer_metrics(state, ui);
540        if self
541            .global_state
542            .settings
543            .interface
544            .toggle_draggable_windows
545        {
546            self.draggable_area(state, &mut events, ui);
547        }
548        events
549    }
550}
551
552widget_ids! {
553    pub struct BackgroundIds {
554        bg,
555        bg_frame,
556    }
557}
558
559#[derive(Copy, Clone, PartialEq, Eq)]
560pub enum BagTab {
561    Gear,
562    Inventory,
563    //Ingredients,
564    //Food,
565    //Quest,
566}
567
568/// A collection of dependencies needed for tabs, intended to reduce parameter
569/// bloat
570pub struct TabPackage<'a> {
571    global_state: &'a GlobalState,
572    client: &'a Client,
573    info: &'a HudInfo<'a>,
574    imgs: &'a Imgs,
575    item_imgs: &'a ItemImgs,
576    fonts: &'a Fonts,
577    localized_strings: &'a Localization,
578    item_i18n: &'a ItemI18n,
579    pulse: f32,
580}
581
582widget_ids! {
583    pub struct BagManagerIds {
584        primary_bag,
585        secondary_bag,
586    }
587}
588
589pub struct BagManagerState {
590    ids: BagManagerIds,
591    bg_ids_primary: BackgroundIds,
592    bg_ids_secondary: BackgroundIds,
593}
594
595pub enum BagEvent {
596    Close,
597    MoveBag(Vec2<f64>),
598    BagExpand,
599    SetDetailsMode(bool),
600    ChangeInventorySortOrder(InventorySortOrder),
601    SortInventory(InventorySortOrder),
602    SwapEquippedWeapons,
603}
604
605#[derive(WidgetCommon)]
606pub struct BagManager<'a> {
607    global_state: &'a GlobalState,
608    client: &'a Client,
609    info: &'a HudInfo<'a>,
610    imgs: &'a Imgs,
611    item_imgs: &'a ItemImgs,
612    fonts: &'a Fonts,
613    #[conrod(common_builder)]
614    common: widget::CommonBuilder,
615    rot_imgs: &'a ImgsRot,
616    tooltip_manager: &'a mut TooltipManager,
617    item_tooltip_manager: &'a mut ItemTooltipManager,
618    slot_manager: &'a mut SlotManager,
619    pulse: f32,
620    localized_strings: &'a Localization,
621    item_i18n: &'a ItemI18n,
622    stats: &'a Stats,
623    skill_set: &'a SkillSet,
624    health: &'a Health,
625    energy: &'a Energy,
626    show: &'a Show,
627    body: &'a Body,
628    msm: &'a MaterialStatManifest,
629    rbm: &'a RecipeBookManifest,
630    poise: &'a Poise,
631    menu_events: &'a Vec<MenuInput>,
632}
633
634impl<'a> BagManager<'a> {
635    /// Manages the primary player inventory window, as well as the secondary
636    /// split screen window. The secondary window only displays the gear tab,
637    /// and will auto collapse when trading
638    #[expect(clippy::too_many_arguments)]
639    pub fn new(
640        global_state: &'a GlobalState,
641        client: &'a Client,
642        info: &'a HudInfo<'a>,
643        imgs: &'a Imgs,
644        item_imgs: &'a ItemImgs,
645        fonts: &'a Fonts,
646        rot_imgs: &'a ImgsRot,
647        tooltip_manager: &'a mut TooltipManager,
648        item_tooltip_manager: &'a mut ItemTooltipManager,
649        slot_manager: &'a mut SlotManager,
650        pulse: f32,
651        localized_strings: &'a Localization,
652        item_i18n: &'a ItemI18n,
653        stats: &'a Stats,
654        skill_set: &'a SkillSet,
655        health: &'a Health,
656        energy: &'a Energy,
657        show: &'a Show,
658        body: &'a Body,
659        msm: &'a MaterialStatManifest,
660        rbm: &'a RecipeBookManifest,
661        poise: &'a Poise,
662        menu_events: &'a Vec<MenuInput>,
663    ) -> Self {
664        Self {
665            global_state,
666            client,
667            info,
668            imgs,
669            item_imgs,
670            fonts,
671            common: widget::CommonBuilder::default(),
672            rot_imgs,
673            tooltip_manager,
674            item_tooltip_manager,
675            slot_manager,
676            pulse,
677            localized_strings,
678            item_i18n,
679            stats,
680            skill_set,
681            health,
682            energy,
683            show,
684            body,
685            msm,
686            rbm,
687            poise,
688            menu_events,
689        }
690    }
691}
692
693impl Widget for BagManager<'_> {
694    type Event = Vec<BagEvent>;
695    type State = BagManagerState;
696    type Style = ();
697
698    fn init_state(&self, mut id_gen: widget::id::Generator) -> Self::State {
699        BagManagerState {
700            bg_ids_primary: BackgroundIds {
701                bg: id_gen.next(),
702                bg_frame: id_gen.next(),
703            },
704            bg_ids_secondary: BackgroundIds {
705                bg: id_gen.next(),
706                bg_frame: id_gen.next(),
707            },
708            ids: BagManagerIds::new(id_gen),
709        }
710    }
711
712    fn style(&self) -> Self::Style {}
713
714    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
715        common_base::prof_span!("Bag::update");
716        let widget::UpdateArgs { state, ui, .. } = args;
717        let mut events = Vec::new();
718
719        // The primary bag
720        for event in BagWindow::new(
721            self.global_state,
722            self.client,
723            self.info,
724            self.imgs,
725            self.item_imgs,
726            self.fonts,
727            self.rot_imgs,
728            self.tooltip_manager,
729            self.item_tooltip_manager,
730            self.slot_manager,
731            self.pulse,
732            self.localized_strings,
733            self.item_i18n,
734            self.stats,
735            self.skill_set,
736            self.health,
737            self.energy,
738            self.show,
739            self.body,
740            self.msm,
741            self.rbm,
742            self.poise,
743            self.menu_events,
744            &state.bg_ids_primary,
745        )
746        .set(state.ids.primary_bag, ui)
747        {
748            events.push(event)
749        }
750
751        // This is the secondary bag, visibile in split screen mode to show gear next to
752        // the inventory; should only be visible for the player, and should not be
753        // visible when trading
754        if self.show.bag_menu_split && !self.show.trade {
755            for event in BagWindow::new(
756                self.global_state,
757                self.client,
758                self.info,
759                self.imgs,
760                self.item_imgs,
761                self.fonts,
762                self.rot_imgs,
763                self.tooltip_manager,
764                self.item_tooltip_manager,
765                self.slot_manager,
766                self.pulse,
767                self.localized_strings,
768                self.item_i18n,
769                self.stats,
770                self.skill_set,
771                self.health,
772                self.energy,
773                self.show,
774                self.body,
775                self.msm,
776                self.rbm,
777                self.poise,
778                self.menu_events,
779                &state.bg_ids_secondary,
780            )
781            .add_secondary(&state.bg_ids_primary)
782            .set(state.ids.secondary_bag, ui)
783            {
784                events.push(event)
785            }
786        }
787
788        events
789    }
790}
791
792widget_ids! {
793    pub struct BagWindowIds {
794        bag_title_anchor,
795        bag_title_dropshadow,
796        bag_title,
797        close_button,
798        tab_anchor,
799        gear_tab,
800        inventory_tab,
801        //ingredients_tab,
802        //food_tab,
803        //quest_tab,
804        gear_tab_content,
805        inventory_tab_content,
806        //ingredients_tab_content,
807        //food_tab_content,
808        //quest_tab_content,
809        left_button,
810        right_button,
811        draggable_area,
812    }
813}
814
815pub struct BagWindowState {
816    ids: BagWindowIds,
817    active_tab: BagTab,
818}
819
820#[derive(WidgetCommon)]
821pub struct BagWindow<'a> {
822    global_state: &'a GlobalState,
823    client: &'a Client,
824    info: &'a HudInfo<'a>,
825    imgs: &'a Imgs,
826    item_imgs: &'a ItemImgs,
827    fonts: &'a Fonts,
828    #[conrod(common_builder)]
829    common: widget::CommonBuilder,
830    rot_imgs: &'a ImgsRot,
831    tooltip_manager: &'a mut TooltipManager,
832    item_tooltip_manager: &'a mut ItemTooltipManager,
833    slot_manager: &'a mut SlotManager,
834    pulse: f32,
835    localized_strings: &'a Localization,
836    item_i18n: &'a ItemI18n,
837    stats: &'a Stats,
838    skill_set: &'a SkillSet,
839    health: &'a Health,
840    energy: &'a Energy,
841    show: &'a Show,
842    body: &'a Body,
843    msm: &'a MaterialStatManifest,
844    rbm: &'a RecipeBookManifest,
845    poise: &'a Poise,
846    menu_events: &'a Vec<MenuInput>,
847    bg_ids: &'a BackgroundIds,
848    is_player: bool,
849    primary_bg_ids: Option<&'a BackgroundIds>,
850}
851
852impl<'a> BagWindow<'a> {
853    builder_methods! {
854        is_player { is_player = bool }
855        add_secondary { primary_bg_ids = Some(&'a BackgroundIds) }
856    }
857
858    /// Creates an inventory/bag window with multiple horizontal tabs used to
859    /// filter the items down into more manageable groups
860    #[expect(clippy::too_many_arguments)]
861    pub fn new(
862        global_state: &'a GlobalState,
863        client: &'a Client,
864        info: &'a HudInfo<'a>,
865        imgs: &'a Imgs,
866        item_imgs: &'a ItemImgs,
867        fonts: &'a Fonts,
868        rot_imgs: &'a ImgsRot,
869        tooltip_manager: &'a mut TooltipManager,
870        item_tooltip_manager: &'a mut ItemTooltipManager,
871        slot_manager: &'a mut SlotManager,
872        pulse: f32,
873        localized_strings: &'a Localization,
874        item_i18n: &'a ItemI18n,
875        stats: &'a Stats,
876        skill_set: &'a SkillSet,
877        health: &'a Health,
878        energy: &'a Energy,
879        show: &'a Show,
880        body: &'a Body,
881        msm: &'a MaterialStatManifest,
882        rbm: &'a RecipeBookManifest,
883        poise: &'a Poise,
884        menu_events: &'a Vec<MenuInput>,
885        bg_ids: &'a BackgroundIds,
886    ) -> Self {
887        Self {
888            global_state,
889            client,
890            info,
891            imgs,
892            item_imgs,
893            fonts,
894            common: widget::CommonBuilder::default(),
895            rot_imgs,
896            tooltip_manager,
897            item_tooltip_manager,
898            slot_manager,
899            pulse,
900            localized_strings,
901            item_i18n,
902            stats,
903            skill_set,
904            health,
905            energy,
906            show,
907            body,
908            msm,
909            rbm,
910            poise,
911            menu_events,
912            bg_ids,
913            is_player: true,
914            primary_bg_ids: None,
915        }
916    }
917
918    fn draggable_area(
919        &self,
920        state: &ConrodState<'_, BagWindowState>,
921        events: &mut Vec<BagEvent>,
922        ui: &mut UiCell<'_>,
923    ) {
924        let bag_settings = &self.global_state.settings.hud_position;
925        let bag_pos = if self.is_player {
926            bag_settings.bag.own
927        } else {
928            bag_settings.bag.other
929        };
930
931        let bag_size: Vec2<f64> = if self.is_player {
932            [DEFAULT_OWN_BAG_WIDTH, DEFAULT_OWN_BAG_HEIGHT].into()
933        } else {
934            [DEFAULT_OTHER_BAG_WIDTH, DEFAULT_OTHER_BAG_HEIGHT].into()
935        };
936
937        let pos_delta: Vec2<f64> = ui
938            .widget_input(state.ids.draggable_area)
939            .drags()
940            .left()
941            .map(|drag| Vec2::<f64>::from(drag.delta_xy))
942            .sum();
943
944        let pos_delta: Vec2<f64> = if self.is_player {
945            // Own (right side) bags use bottom_right_with_margins_on
946            // which means we have to use positive margins to move left
947            // so we have to invert the x value from the delta
948            pos_delta.with_x(-pos_delta.x)
949        } else {
950            // Others (left side) bags use bottom_left_with_margins_on
951            pos_delta
952        };
953
954        let window_clamp = Vec2::new(ui.win_w, ui.win_h) - bag_size;
955
956        let new_pos = (bag_pos + pos_delta)
957            .map(|e| e.max(0.))
958            .map2(window_clamp, |e, bounds| e.min(bounds));
959
960        if new_pos.abs_diff_ne(&bag_pos, f64::EPSILON) {
961            events.push(BagEvent::MoveBag(new_pos));
962        }
963
964        if ui
965            .widget_input(state.ids.draggable_area)
966            .clicks()
967            .right()
968            .count()
969            == 1
970        {
971            events.push(BagEvent::MoveBag(if self.is_player {
972                HudPositionSettings::default().bag.own
973            } else {
974                HudPositionSettings::default().bag.other
975            }));
976        }
977
978        Rectangle::fill_with([424.0, 71.0], color::TRANSPARENT)
979            .top_left_with_margin_on(self.bg_ids.bg_frame, 0.0)
980            .set(state.ids.draggable_area, ui);
981    }
982}
983
984impl Widget for BagWindow<'_> {
985    type Event = Vec<BagEvent>;
986    type State = BagWindowState;
987    type Style = ();
988
989    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
990        BagWindowState {
991            ids: BagWindowIds::new(id_gen),
992            active_tab: BagTab::Inventory,
993        }
994    }
995
996    fn style(&self) -> Self::Style {}
997
998    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
999        common_base::prof_span!("BagWindow::update");
1000        let widget::UpdateArgs { state, ui, .. } = args;
1001        let _i18n = &self.localized_strings;
1002        let mut events = Vec::new();
1003
1004        // If no primary bag ids were passed in, assume it is secondary bag
1005        let is_primary = self.primary_bg_ids.is_none();
1006        // The gear tab should not be opened in the primary window if using split screen
1007        // When trading, split screen is temporarily minimized
1008        let block_gear_tab = self.show.bag_menu_split && !self.show.trade;
1009
1010        // MENU INPUTS: change bag tabs
1011        // PageDown: try to go left a tab (wrap)
1012        // PageUp: try to go right a tab (wrap)
1013        if !is_primary {
1014            // Secondary window is always the gear tab
1015            if state.active_tab != BagTab::Gear {
1016                state.update(|s| {
1017                    s.active_tab = BagTab::Gear;
1018                })
1019            }
1020        } else {
1021            // Changes tabs on primary window
1022            for event in self.menu_events {
1023                match *event {
1024                    MenuInput::PageDown => {
1025                        state.update(|s| {
1026                            if s.active_tab == BagTab::Inventory {
1027                                s.active_tab = BagTab::Gear;
1028                            } else {
1029                                s.active_tab = BagTab::Inventory;
1030                            }
1031
1032                            self.slot_manager.idle();
1033                        });
1034                    },
1035                    MenuInput::PageUp => state.update(|s| {
1036                        if s.active_tab == BagTab::Gear {
1037                            s.active_tab = BagTab::Inventory;
1038                        } else {
1039                            s.active_tab = BagTab::Gear;
1040                        }
1041
1042                        self.slot_manager.idle();
1043                    }),
1044                    // All other events are handled by child widgets
1045                    _ => {},
1046                }
1047            }
1048        }
1049
1050        // Block any attempts to open the gear screen when it shouldn't be open (i.e.,
1051        // split screen is being used)
1052        if block_gear_tab && state.active_tab == BagTab::Gear && is_primary {
1053            state.update(|s| {
1054                s.active_tab = BagTab::Inventory;
1055            })
1056        }
1057
1058        // Background image/frame
1059        let bag_pos = if self.is_player {
1060            &self.global_state.settings.hud_position.bag.own
1061        } else {
1062            &self.global_state.settings.hud_position.bag.other
1063        };
1064        let bg_img = if self.is_player {
1065            self.imgs.player_inv_bg_bag
1066        } else {
1067            // TODO: Create other background when InventoryScroller is removed
1068            self.imgs.player_inv_bg_bag
1069        };
1070        let bg_frame_img = self.imgs.player_inv_frame_bag;
1071
1072        let mut bg = Image::new(bg_img).w_h(424.0, 700.0).color(Some(UI_MAIN));
1073        if self.is_player && is_primary {
1074            bg = bg.bottom_right_with_margins_on(ui.window, bag_pos.y, bag_pos.x);
1075        } else if !self.is_player {
1076            bg = bg.bottom_left_with_margins_on(ui.window, bag_pos.y, bag_pos.x);
1077        } else {
1078            // Split window should be rendered left of the primary window
1079            if let Some(primary_ids) = self.primary_bg_ids {
1080                bg = bg.left_from(primary_ids.bg, 0.0);
1081            } else {
1082                // Fallback---shouldn't ever be called, but just in case
1083                // Only rendered next to players inventory, so always bottom right
1084                bg = bg.bottom_right_with_margins_on(ui.window, bag_pos.y, bag_pos.x)
1085            }
1086        }
1087        bg.set(self.bg_ids.bg, ui);
1088
1089        Image::new(bg_frame_img)
1090            .w_h(424.0, 700.0)
1091            .middle_of(self.bg_ids.bg)
1092            .color(Some(UI_HIGHLIGHT_0))
1093            .set(self.bg_ids.bg_frame, ui);
1094
1095        // Window title
1096        let title_txt = match state.active_tab {
1097            BagTab::Gear => &self.localized_strings.get_msg("hud-bag-gear-tab"),
1098            BagTab::Inventory => &self.localized_strings.get_msg("gameinput-inventory"),
1099            //BagTab::Ingredients => &self.localized_strings.get_msg("hud-bag-ingredients-tab"),
1100            //BagTab::Food => &self.localized_strings.get_msg("hud-crafting-tabs-food"),
1101            //BagTab::Quest => &self.localized_strings.get_msg("hud-bag-quest-items-tab"),
1102        };
1103        Rectangle::fill_with([0.0, 0.0], color::TRANSPARENT)
1104            .mid_top_with_margin_on(self.bg_ids.bg_frame, 16.5)
1105            .set(state.ids.bag_title_anchor, ui);
1106        Text::new(title_txt)
1107            .x_y_relative_to(state.ids.bag_title_anchor, -2.0, -2.0)
1108            .font_id(self.fonts.cyri.conrod_id)
1109            .font_size(self.fonts.cyri.scale(22))
1110            .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
1111            .set(state.ids.bag_title_dropshadow, ui);
1112        Text::new(title_txt)
1113            .x_y_relative_to(state.ids.bag_title_dropshadow, 2.0, 2.0)
1114            .font_id(self.fonts.cyri.conrod_id)
1115            .font_size(self.fonts.cyri.scale(22))
1116            .color(TEXT_COLOR)
1117            .set(state.ids.bag_title, ui);
1118
1119        // Draggable window space
1120        if self
1121            .global_state
1122            .settings
1123            .interface
1124            .toggle_draggable_windows
1125        {
1126            self.draggable_area(state, &mut events, ui);
1127        }
1128
1129        if is_primary {
1130            // Close button
1131            if Button::image(self.imgs.close_btn)
1132                .w_h(24.0, 25.0)
1133                .hover_image(self.imgs.close_btn_hover)
1134                .press_image(self.imgs.close_btn_press)
1135                .top_right_of(self.bg_ids.bg_frame)
1136                .set(state.ids.close_button, ui)
1137                .was_clicked()
1138            {
1139                events.push(BagEvent::Close);
1140            }
1141
1142            let tab_space = 12.5;
1143
1144            // Tab anchor
1145            Rectangle::fill_with([0.0, 0.0], color::TRANSPARENT)
1146                .mid_top_with_margin_on(self.bg_ids.bg_frame, 35.5)
1147                .set(state.ids.tab_anchor, ui);
1148
1149            // Inventory tab
1150            let tab_img = if state.active_tab == BagTab::Inventory {
1151                self.imgs.bag_tab_press
1152            } else {
1153                self.imgs.bag_tab
1154            };
1155            if Button::image(tab_img)
1156                .w_h(30.0, 30.0)
1157                .right_from(state.ids.tab_anchor, tab_space / 2.0)
1158                .set(state.ids.inventory_tab, ui)
1159                .was_clicked()
1160            {
1161                state.update(|s| {
1162                    s.active_tab = BagTab::Inventory;
1163                })
1164            }
1165
1166            // Gear tab
1167            let tab_img = if state.active_tab == BagTab::Gear || block_gear_tab {
1168                self.imgs.gear_tab_press
1169            } else {
1170                self.imgs.gear_tab
1171            };
1172            if Button::image(tab_img)
1173                .w_h(30.0, 30.0)
1174                .left_from(state.ids.tab_anchor, tab_space / 2.0)
1175                .set(state.ids.gear_tab, ui)
1176                .was_clicked()
1177            {
1178                // Can't open gear tab when using split screen
1179                if is_primary && !block_gear_tab {
1180                    state.update(|s| {
1181                        s.active_tab = BagTab::Gear;
1182                    })
1183                }
1184            }
1185
1186            // Left and right menu button inputs
1187            let right_key = match self.global_state.window.last_input() {
1188                LastInput::Controller => icon_utils::get_controller_input_string_menu(
1189                    MenuInput::PageUp,
1190                    &self.global_state.settings,
1191                    self.global_state.window.controller_type(),
1192                )
1193                .unwrap_or_default(),
1194                LastInput::Keyboard | LastInput::Mouse => self
1195                    .global_state
1196                    .settings
1197                    .controls
1198                    .get_menu_binding(MenuInput::PageUp)
1199                    .map_or_else(|| "".into(), |key| key.display_string()),
1200            };
1201            let left_key = match self.global_state.window.last_input() {
1202                LastInput::Controller => icon_utils::get_controller_input_string_menu(
1203                    MenuInput::PageDown,
1204                    &self.global_state.settings,
1205                    self.global_state.window.controller_type(),
1206                )
1207                .unwrap_or_default(),
1208                LastInput::Keyboard | LastInput::Mouse => self
1209                    .global_state
1210                    .settings
1211                    .controls
1212                    .get_menu_binding(MenuInput::PageDown)
1213                    .map_or_else(|| "".into(), |key| key.display_string()),
1214            };
1215
1216            RichText::new(&left_key, self.imgs)
1217                .left_from(state.ids.gear_tab, tab_space)
1218                .font_id(self.fonts.cyri.conrod_id)
1219                .font_size(self.fonts.cyri.scale(22))
1220                .color(TEXT_COLOR)
1221                .set(state.ids.left_button, ui);
1222
1223            RichText::new(&right_key, self.imgs)
1224                .right_from(state.ids.inventory_tab, tab_space)
1225                .font_id(self.fonts.cyri.conrod_id)
1226                .font_size(self.fonts.cyri.scale(22))
1227                .color(TEXT_COLOR)
1228                .set(state.ids.right_button, ui);
1229        }
1230
1231        // Display tab contents in remaining space
1232        if let Some(inventory) = self.client.inventories().get(self.info.viewpoint_entity) {
1233            // Tooltips
1234            let tooltip = Tooltip::new({
1235                // Edge images [t, b, r, l]
1236                // Corner images [tr, tl, br, bl]
1237                let edge = &self.rot_imgs.tt_side;
1238                let corner = &self.rot_imgs.tt_corner;
1239                ImageFrame::new(
1240                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
1241                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
1242                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
1243                    5.0,
1244                )
1245            })
1246            .title_font_size(self.fonts.cyri.scale(15))
1247            .parent(ui.window)
1248            .desc_font_size(self.fonts.cyri.scale(12))
1249            .font_id(self.fonts.cyri.conrod_id)
1250            .desc_text_color(TEXT_COLOR);
1251
1252            let bag_tooltip = Tooltip::new({
1253                // Edge images [t, b, r, l]
1254                // Corner images [tr, tl, br, bl]
1255                let edge = &self.rot_imgs.tt_side;
1256                let corner = &self.rot_imgs.tt_corner;
1257                ImageFrame::new(
1258                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
1259                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
1260                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
1261                    5.0,
1262                )
1263            })
1264            .title_font_size(self.fonts.cyri.scale(15))
1265            .parent(ui.window)
1266            .desc_font_size(self.fonts.cyri.scale(12))
1267            .font_id(self.fonts.cyri.conrod_id)
1268            .desc_text_color(TEXT_COLOR);
1269
1270            let item_tooltip = ItemTooltip::new(
1271                {
1272                    // Edge images [t, b, r, l]
1273                    // Corner images [tr, tl, br, bl]
1274                    let edge = &self.rot_imgs.tt_side;
1275                    let corner = &self.rot_imgs.tt_corner;
1276                    ImageFrame::new(
1277                        [edge.cw180, edge.none, edge.cw270, edge.cw90],
1278                        [corner.none, corner.cw270, corner.cw90, corner.cw180],
1279                        Color::Rgba(0.08, 0.07, 0.04, 1.0),
1280                        5.0,
1281                    )
1282                },
1283                self.client,
1284                self.info,
1285                self.imgs,
1286                self.item_imgs,
1287                self.pulse,
1288                self.msm,
1289                self.rbm,
1290                Some(inventory),
1291                self.localized_strings,
1292                self.item_i18n,
1293            )
1294            .title_font_size(self.fonts.cyri.scale(20))
1295            .parent(ui.window)
1296            .desc_font_size(self.fonts.cyri.scale(12))
1297            .font_id(self.fonts.cyri.conrod_id)
1298            .desc_text_color(TEXT_COLOR);
1299
1300            let tab_package = TabPackage {
1301                global_state: self.global_state,
1302                client: self.client,
1303                info: self.info,
1304                imgs: self.imgs,
1305                item_imgs: self.item_imgs,
1306                fonts: self.fonts,
1307                localized_strings: self.localized_strings,
1308                item_i18n: self.item_i18n,
1309                pulse: self.pulse,
1310            };
1311
1312            match state.active_tab {
1313                BagTab::Gear => {
1314                    // Gear tab
1315                    for event in GearMenu::new(
1316                        &tab_package,
1317                        inventory,
1318                        &tooltip,
1319                        &bag_tooltip,
1320                        &item_tooltip,
1321                        self.tooltip_manager,
1322                        self.item_tooltip_manager,
1323                        self.slot_manager,
1324                        self.stats,
1325                        self.skill_set,
1326                        self.health,
1327                        self.energy,
1328                        self.show,
1329                        self.body,
1330                        self.msm,
1331                        self.poise,
1332                        self.menu_events,
1333                        self.bg_ids,
1334                    )
1335                    .set(state.ids.gear_tab_content, ui)
1336                    {
1337                        match event {
1338                            GearMenuEvent::Close => events.push(BagEvent::Close),
1339                            GearMenuEvent::SwapEquippedWeapons => {
1340                                events.push(BagEvent::SwapEquippedWeapons)
1341                            },
1342                        }
1343                    }
1344                },
1345                BagTab::Inventory => {
1346                    // Inventory tab
1347                    for event in InventoryMenu::new(
1348                        &tab_package,
1349                        inventory,
1350                        &tooltip,
1351                        &bag_tooltip,
1352                        &item_tooltip,
1353                        self.tooltip_manager,
1354                        self.item_tooltip_manager,
1355                        self.slot_manager,
1356                        self.menu_events,
1357                        self.bg_ids,
1358                        self.show.bag_details,
1359                        self.show.crafting_fields.salvage,
1360                    )
1361                    .expanded_window(self.show.bag_menu_split)
1362                    .trading(self.show.trade)
1363                    .set(state.ids.inventory_tab_content, ui)
1364                    {
1365                        match event {
1366                            InventoryMenuEvent::Close => events.push(BagEvent::Close),
1367                            InventoryMenuEvent::BagExpand => events.push(BagEvent::BagExpand),
1368                            InventoryMenuEvent::SetDetailsMode => {
1369                                events.push(BagEvent::SetDetailsMode(!self.show.bag_details))
1370                            },
1371                            InventoryMenuEvent::ChangeInventorySortOrder => {
1372                                events.push(BagEvent::ChangeInventorySortOrder(
1373                                    self.global_state.settings.inventory.sort_order.next(),
1374                                ))
1375                            },
1376                            InventoryMenuEvent::SortInventory => {
1377                                events.push(BagEvent::SortInventory(
1378                                    self.global_state.settings.inventory.sort_order,
1379                                ));
1380                            },
1381                        }
1382                    }
1383                },
1384            }
1385        }
1386
1387        events
1388    }
1389}
1390
1391widget_ids! {
1392    pub struct InventoryMenuIds {
1393        scrollbar_bg,
1394        scrollbar_slots,
1395        inv_alignment,
1396        slot_grid,
1397        bag_details_btn,
1398        inventory_sort,
1399        inventory_sort_selected,
1400        bag_expand_btn,
1401        space_txt,
1402        scroll_divider,
1403        spacing_above,
1404    }
1405}
1406
1407pub struct InventoryMenuState {
1408    ids: InventoryMenuIds,
1409}
1410
1411pub enum InventoryMenuEvent {
1412    Close,
1413    BagExpand,
1414    SetDetailsMode,
1415    ChangeInventorySortOrder,
1416    SortInventory,
1417}
1418
1419#[derive(WidgetCommon)]
1420pub struct InventoryMenu<'a> {
1421    tab_package: &'a TabPackage<'a>,
1422    inventory: &'a Inventory,
1423    tooltip: &'a Tooltip<'a>,
1424    bag_tooltip: &'a Tooltip<'a>,
1425    item_tooltip: &'a ItemTooltip<'a>,
1426    #[conrod(common_builder)]
1427    common: widget::CommonBuilder,
1428    tooltip_manager: &'a mut TooltipManager,
1429    item_tooltip_manager: &'a mut ItemTooltipManager,
1430    slot_manager: &'a mut SlotManager,
1431    menu_events: &'a Vec<MenuInput>,
1432    bag_ids: &'a BackgroundIds,
1433    details_mode: bool,
1434    show_salvage: bool,
1435    filter: TabFilters,
1436    expanded_window: bool,
1437    trading: bool,
1438}
1439
1440impl<'a> InventoryMenu<'a> {
1441    builder_methods! {
1442        pub filter { filter = TabFilters }
1443        pub expanded_window { expanded_window = bool }
1444        pub trading { trading = bool }
1445    }
1446
1447    /// Displays the contents of an inventory, can be filtered
1448    pub fn new(
1449        tab_package: &'a TabPackage<'a>,
1450        inventory: &'a Inventory,
1451        tooltip: &'a Tooltip<'a>,
1452        bag_tooltip: &'a Tooltip<'a>,
1453        item_tooltip: &'a ItemTooltip<'a>,
1454        tooltip_manager: &'a mut TooltipManager,
1455        item_tooltip_manager: &'a mut ItemTooltipManager,
1456        slot_manager: &'a mut SlotManager,
1457        menu_events: &'a Vec<MenuInput>,
1458        bag_ids: &'a BackgroundIds,
1459        details_mode: bool,
1460        show_salvage: bool,
1461    ) -> Self {
1462        Self {
1463            tab_package,
1464            inventory,
1465            tooltip,
1466            bag_tooltip,
1467            item_tooltip,
1468            common: widget::CommonBuilder::default(),
1469            tooltip_manager,
1470            item_tooltip_manager,
1471            slot_manager,
1472            menu_events,
1473            bag_ids,
1474            details_mode,
1475            show_salvage,
1476            filter: TabFilters::None,
1477            expanded_window: false,
1478            trading: false,
1479        }
1480    }
1481}
1482
1483impl Widget for InventoryMenu<'_> {
1484    type Event = Vec<InventoryMenuEvent>;
1485    type State = InventoryMenuState;
1486    type Style = ();
1487
1488    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
1489        InventoryMenuState {
1490            ids: InventoryMenuIds::new(id_gen),
1491        }
1492    }
1493
1494    fn style(&self) -> Self::Style {}
1495
1496    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
1497        common_base::prof_span!("InventoryMenu::update");
1498        let widget::UpdateArgs { state, ui, .. } = args;
1499        let i18n = &self.tab_package.localized_strings;
1500        let mut events = Vec::new();
1501
1502        // MENU INPUTS:
1503        /*
1504        for event in self.menu_events {
1505            if *event == MenuInput::Back {
1506                events.push(InventoryMenuEvent::Close);
1507            }
1508        }*/
1509
1510        // The grid width and item slot size are all pixel perferct in their alignment
1511        // However, the slot spacing has a few pixel mismatches, but shouldn't be
1512        // noticable
1513        let grid_width = 376.0;
1514        let grid_height = 586.0;
1515
1516        // Alignment for Grid
1517        Rectangle::fill_with([grid_width, grid_height], color::TRANSPARENT)
1518            .mid_bottom_with_margin_on(self.bag_ids.bg_frame, 3.5)
1519            .scroll_kids_vertically()
1520            .set(state.ids.inv_alignment, ui);
1521
1522        // Adds spacing above the slots to make scrolling feel a bit more natural
1523        Rectangle::fill_with([grid_width, 5.0], color::TRANSPARENT)
1524            .mid_top_of(state.ids.inv_alignment)
1525            .set(state.ids.spacing_above, ui);
1526
1527        let mut space_max = 0;
1528        let compact_mode = self
1529            .tab_package
1530            .global_state
1531            .settings
1532            .interface
1533            .toggle_compact_item_slots;
1534        let columns = if compact_mode { 9 } else { 6 };
1535        let spacing = if compact_mode { 2.0 } else { 5.8 };
1536        let slot_size = if compact_mode { 40.0 } else { 57.8 };
1537        let scroll_size = if compact_mode { 117 } else { 54 };
1538
1539        // Bag Slots
1540        for event in SlotGrid::new(
1541            self.tab_package.client,
1542            self.tab_package.global_state,
1543            self.tab_package.imgs,
1544            self.tab_package.item_imgs,
1545            self.tab_package.fonts,
1546            self.item_tooltip_manager,
1547            self.slot_manager,
1548            self.inventory,
1549            self.item_tooltip,
1550            self.tab_package.localized_strings,
1551            self.tab_package.item_i18n,
1552            self.tab_package.info.viewpoint_entity,
1553            self.tab_package.pulse,
1554            self.menu_events,
1555            true, // is_us
1556            self.details_mode,
1557            self.show_salvage,
1558        )
1559        .columns(columns)
1560        .spacing(if self.details_mode { 0.0 } else { spacing })
1561        .slot_size(if self.details_mode { 20.0 } else { slot_size })
1562        .filter(self.filter)
1563        .w_of(state.ids.inv_alignment)
1564        .down_from(state.ids.spacing_above, 0.0)
1565        .set(state.ids.slot_grid, ui)
1566        {
1567            match event {
1568                SlotEvents::Close => events.push(InventoryMenuEvent::Close),
1569                SlotEvents::FilteredSize(size) => {
1570                    space_max = size;
1571                },
1572                _ => {},
1573            }
1574        }
1575
1576        // Slots scrollbar
1577        if space_max > scroll_size {
1578            // Scrollbar-BG
1579            Image::new(self.tab_package.imgs.scrollbar_bg_big)
1580                .w_h(9.0, 577.0)
1581                .bottom_right_with_margins_on(self.bag_ids.bg_frame, 24.0, 3.0)
1582                .color(Some(UI_HIGHLIGHT_0))
1583                .set(state.ids.scrollbar_bg, ui);
1584            // Scrollbar
1585            Scrollbar::y_axis(state.ids.inv_alignment)
1586                .thickness(6.0)
1587                .h(530.0)
1588                .color(UI_MAIN)
1589                .middle_of(state.ids.scrollbar_bg)
1590                .set(state.ids.scrollbar_slots, ui);
1591        };
1592
1593        // A visual divider above the item grid
1594        Rectangle::fill_with([grid_width, 1.5], Color::Rgba(0.314, 0.443, 0.443, 1.0))
1595            .up_from(state.ids.inv_alignment, 0.0)
1596            .set(state.ids.scroll_divider, ui);
1597
1598        // Top buttons
1599        //
1600        // Toggle the split screen gear|inventory window
1601        if !self.trading {
1602            // Don't show button when trading
1603            if Button::image(if self.expanded_window {
1604                self.tab_package.imgs.expand_btn
1605            } else {
1606                self.tab_package.imgs.collapse_btn
1607            })
1608            .w_h(30.0, 17.0)
1609            .hover_image(if self.expanded_window {
1610                self.tab_package.imgs.expand_btn_hover
1611            } else {
1612                self.tab_package.imgs.collapse_btn_hover
1613            })
1614            .press_image(if self.expanded_window {
1615                self.tab_package.imgs.expand_btn_press
1616            } else {
1617                self.tab_package.imgs.collapse_btn_press
1618            })
1619            .align_left_of(state.ids.scroll_divider)
1620            .up_from(state.ids.scroll_divider, 5.0)
1621            .with_tooltip(
1622                self.tooltip_manager,
1623                &i18n.get_msg("hud-bag-toggle-expanded-window"),
1624                "",
1625                self.tooltip,
1626                color::WHITE,
1627            )
1628            .set(state.ids.bag_expand_btn, ui)
1629            .was_clicked()
1630            {
1631                events.push(InventoryMenuEvent::BagExpand);
1632            }
1633        }
1634
1635        // Sort inventory button with selected mode -- middle button
1636        if Button::image(self.tab_package.imgs.inv_sort_selected_btn)
1637            .w_h(30.0, 17.0)
1638            .hover_image(self.tab_package.imgs.inv_sort_selected_btn_hover)
1639            .press_image(self.tab_package.imgs.inv_sort_selected_btn_press)
1640            .align_middle_x_of(state.ids.scroll_divider)
1641            .up_from(state.ids.scroll_divider, 5.0)
1642            .with_tooltip(
1643                self.tooltip_manager,
1644                &(match self.tab_package.global_state.settings.inventory.sort_order {
1645                    InventorySortOrder::Name => i18n.get_msg("hud-bag-sort_by_name"),
1646                    InventorySortOrder::Quality => i18n.get_msg("hud-bag-sort_by_quality"),
1647                    InventorySortOrder::Category => i18n.get_msg("hud-bag-sort_by_category"),
1648                    InventorySortOrder::Tag => i18n.get_msg("hud-bag-sort_by_tag"),
1649                    InventorySortOrder::Amount => i18n.get_msg("hud-bag-sort_by_quantity"),
1650                }),
1651                "",
1652                self.tooltip,
1653                color::WHITE,
1654            )
1655            .set(state.ids.inventory_sort_selected, ui)
1656            .was_clicked()
1657        {
1658            events.push(InventoryMenuEvent::SortInventory);
1659        }
1660
1661        // Sort mode inventory button -- left button
1662        if Button::image(self.tab_package.imgs.inv_sort_btn)
1663            .w_h(30.0, 17.0)
1664            .hover_image(self.tab_package.imgs.inv_sort_btn_hover)
1665            .press_image(self.tab_package.imgs.inv_sort_btn_press)
1666            .left_from(state.ids.inventory_sort_selected, 10.0)
1667            .with_tooltip(
1668                self.tooltip_manager,
1669                &(match self
1670                    .tab_package
1671                    .global_state
1672                    .settings
1673                    .inventory
1674                    .sort_order
1675                    .next()
1676                {
1677                    InventorySortOrder::Name => i18n.get_msg("hud-bag-change_to_sort_by_name"),
1678                    InventorySortOrder::Quality => {
1679                        i18n.get_msg("hud-bag-change_to_sort_by_quality")
1680                    },
1681                    InventorySortOrder::Category => {
1682                        i18n.get_msg("hud-bag-change_to_sort_by_category")
1683                    },
1684                    InventorySortOrder::Tag => i18n.get_msg("hud-bag-change_to_sort_by_tag"),
1685                    InventorySortOrder::Amount => {
1686                        i18n.get_msg("hud-bag-change_to_sort_by_quantity")
1687                    },
1688                }),
1689                "",
1690                self.tooltip,
1691                color::WHITE,
1692            )
1693            .set(state.ids.inventory_sort, ui)
1694            .was_clicked()
1695        {
1696            events.push(InventoryMenuEvent::ChangeInventorySortOrder);
1697        }
1698
1699        // Button to toggle grid/list mode -- right button
1700        let (txt, btn, hover, press) = if self.details_mode {
1701            (
1702                "Grid mode",
1703                self.tab_package.imgs.grid_btn,
1704                self.tab_package.imgs.grid_btn_hover,
1705                self.tab_package.imgs.grid_btn_press,
1706            )
1707        } else {
1708            (
1709                "List mode",
1710                self.tab_package.imgs.list_btn,
1711                self.tab_package.imgs.list_btn_hover,
1712                self.tab_package.imgs.list_btn_press,
1713            )
1714        };
1715        if Button::image(btn)
1716            .w_h(32.0, 17.0)
1717            .hover_image(hover)
1718            .press_image(press)
1719            .right_from(state.ids.inventory_sort_selected, 10.0)
1720            .with_tooltip(self.tooltip_manager, txt, "", self.bag_tooltip, TEXT_COLOR)
1721            .set(state.ids.bag_details_btn, ui)
1722            .was_clicked()
1723        {
1724            events.push(InventoryMenuEvent::SetDetailsMode);
1725        }
1726
1727        // Inventory space text
1728        let space_used = self.inventory.populated_slots();
1729        let space_max = self.inventory.slots().count();
1730        let bag_space = format!("{}/{}", space_used, space_max);
1731        let bag_space_percentage = space_used as f32 / space_max as f32;
1732        Text::new(&bag_space)
1733            .align_right_of(state.ids.scroll_divider)
1734            .up_from(state.ids.scroll_divider, 7.5)
1735            .font_id(self.tab_package.fonts.cyri.conrod_id)
1736            .font_size(self.tab_package.fonts.cyri.scale(14))
1737            .color(if bag_space_percentage < 0.8 {
1738                TEXT_COLOR
1739            } else if bag_space_percentage < 1.0 {
1740                LOW_HP_COLOR
1741            } else {
1742                CRITICAL_HP_COLOR
1743            })
1744            .set(state.ids.space_txt, ui);
1745
1746        events
1747    }
1748}
1749
1750widget_ids! {
1751    pub struct GearMenuIds {
1752        inv_alignment,
1753        slot_grid,
1754        scrollbar_bg,
1755        scrollbar_slots,
1756        scroll_divider,
1757        spacing_above,
1758        // Armor slots
1759        head_slot,
1760        neck_slot,
1761        chest_slot,
1762        shoulders_slot,
1763        hands_slot,
1764        legs_slot,
1765        belt_slot,
1766        lantern_slot,
1767        ring1_slot,
1768        ring2_slot,
1769        feet_slot,
1770        back_slot,
1771        tabard_slot,
1772        glider_slot,
1773        active_mainhand_slot,
1774        active_offhand_slot,
1775        inactive_mainhand_slot,
1776        inactive_offhand_slot,
1777        swap_equipped_weapons_btn,
1778        bag1_slot,
1779        bag2_slot,
1780        bag3_slot,
1781        bag4_slot,
1782        // Stats
1783        stat_icons[],
1784        stat_txts[],
1785    }
1786}
1787
1788pub struct GearMenuState {
1789    ids: GearMenuIds,
1790    is_focused: bool,
1791    active_gear_slot: usize,
1792}
1793
1794pub enum GearMenuEvent {
1795    Close,
1796    SwapEquippedWeapons,
1797}
1798
1799#[derive(WidgetCommon)]
1800pub struct GearMenu<'a> {
1801    tab_package: &'a TabPackage<'a>,
1802    inventory: &'a Inventory,
1803    tooltip: &'a Tooltip<'a>,
1804    bag_tooltip: &'a Tooltip<'a>,
1805    item_tooltip: &'a ItemTooltip<'a>,
1806    #[conrod(common_builder)]
1807    common: widget::CommonBuilder,
1808    tooltip_manager: &'a mut TooltipManager,
1809    item_tooltip_manager: &'a mut ItemTooltipManager,
1810    slot_manager: &'a mut SlotManager,
1811    stats: &'a Stats,
1812    skill_set: &'a SkillSet,
1813    health: &'a Health,
1814    energy: &'a Energy,
1815    show: &'a Show,
1816    body: &'a Body,
1817    msm: &'a MaterialStatManifest,
1818    poise: &'a Poise,
1819    menu_events: &'a Vec<MenuInput>,
1820    bag_ids: &'a BackgroundIds,
1821}
1822
1823impl<'a> GearMenu<'a> {
1824    /// Displays an entities equipped gear, while showing their stats (and gear
1825    /// inventory as well if viewed as a bag tab)
1826    #[expect(clippy::too_many_arguments)]
1827    pub fn new(
1828        tab_package: &'a TabPackage,
1829        inventory: &'a Inventory,
1830        tooltip: &'a Tooltip<'a>,
1831        bag_tooltip: &'a Tooltip<'a>,
1832        item_tooltip: &'a ItemTooltip<'a>,
1833        tooltip_manager: &'a mut TooltipManager,
1834        item_tooltip_manager: &'a mut ItemTooltipManager,
1835        slot_manager: &'a mut SlotManager,
1836        stats: &'a Stats,
1837        skill_set: &'a SkillSet,
1838        health: &'a Health,
1839        energy: &'a Energy,
1840        show: &'a Show,
1841        body: &'a Body,
1842        msm: &'a MaterialStatManifest,
1843        poise: &'a Poise,
1844        menu_events: &'a Vec<MenuInput>,
1845        bag_ids: &'a BackgroundIds,
1846    ) -> Self {
1847        Self {
1848            tab_package,
1849            inventory,
1850            tooltip,
1851            bag_tooltip,
1852            item_tooltip,
1853            common: widget::CommonBuilder::default(),
1854            tooltip_manager,
1855            item_tooltip_manager,
1856            slot_manager,
1857            stats,
1858            skill_set,
1859            health,
1860            energy,
1861            show,
1862            body,
1863            msm,
1864            poise,
1865            menu_events,
1866            bag_ids,
1867        }
1868    }
1869}
1870
1871impl Widget for GearMenu<'_> {
1872    type Event = Vec<GearMenuEvent>;
1873    type State = GearMenuState;
1874    type Style = ();
1875
1876    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
1877        GearMenuState {
1878            ids: GearMenuIds::new(id_gen),
1879            is_focused: false,
1880            active_gear_slot: 2,
1881        }
1882    }
1883
1884    fn style(&self) -> Self::Style {}
1885
1886    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
1887        common_base::prof_span!("GearMenu::update");
1888        let widget::UpdateArgs { state, ui, .. } = args;
1889        let i18n = &self.tab_package.localized_strings;
1890        let mut events = Vec::new();
1891
1892        // MENU INPUTS: manage equipped gear
1893        // Up: try to go up one
1894        // Down: try to go down one
1895        // Left: try to go left one
1896        // Right: try to move right one
1897        // Apply: select highlighed gear slot
1898        // Back: closes the bag
1899        if state.is_focused {
1900            for event in self.menu_events {
1901                match *event {
1902                    MenuInput::Up => state.update(|s| {
1903                        // So many values to manual set...
1904                        match s.active_gear_slot {
1905                            // weapon switch button
1906                            0 => {},
1907                            // primary weapon left
1908                            1 => s.active_gear_slot = 5,
1909                            // secondary weapon left
1910                            2 => s.active_gear_slot = 6,
1911                            // secondary weapon right
1912                            3 => s.active_gear_slot = 6,
1913                            // primary weapon right
1914                            4 => s.active_gear_slot = 7,
1915                            // back
1916                            5 => s.active_gear_slot = 8,
1917                            // pants
1918                            6 => s.active_gear_slot = 9,
1919                            // shoes
1920                            7 => s.active_gear_slot = 10,
1921                            // jewelry left
1922                            8 => s.active_gear_slot = 11,
1923                            // belt
1924                            9 => s.active_gear_slot = 12,
1925                            // jewelry right
1926                            10 => s.active_gear_slot = 13,
1927                            // shoulder
1928                            11 => s.active_gear_slot = 14,
1929                            // chest
1930                            12 => s.active_gear_slot = 14,
1931                            // hands/gloves
1932                            13 => s.active_gear_slot = 14,
1933                            // jewelry center
1934                            14 => s.active_gear_slot = 15,
1935                            // hat
1936                            15 => {},
1937                            // tabard
1938                            16 => s.active_gear_slot = 17,
1939                            // glider
1940                            17 => s.active_gear_slot = 18,
1941                            // lantern
1942                            18 => {},
1943                            // Bag 1
1944                            19 => s.active_gear_slot = 16,
1945                            // Bag 2
1946                            20 => s.active_gear_slot = 19,
1947                            // Bag 3
1948                            21 => s.active_gear_slot = 20,
1949                            // Bag 4
1950                            22 => s.active_gear_slot = 21,
1951                            // reset to 1 if unexpected
1952                            _ => s.active_gear_slot = 1,
1953                        }
1954                    }),
1955                    MenuInput::Down => state.update(|s| {
1956                        match s.active_gear_slot {
1957                            // weapon switch button
1958                            0 => s.is_focused = false,
1959                            // primary weapon 1
1960                            1 => s.is_focused = false,
1961                            // secondary weapon 1
1962                            2 => s.is_focused = false,
1963                            // secondary weapon 2
1964                            3 => s.is_focused = false,
1965                            // primary weapon 2
1966                            4 => s.is_focused = false,
1967                            // back
1968                            5 => s.active_gear_slot = 1,
1969                            // pants
1970                            6 => s.active_gear_slot = 2,
1971                            // shoes
1972                            7 => s.active_gear_slot = 4,
1973                            // jewelry left
1974                            8 => s.active_gear_slot = 5,
1975                            // belt
1976                            9 => s.active_gear_slot = 6,
1977                            // jewelry right
1978                            10 => s.active_gear_slot = 7,
1979                            // shoulder
1980                            11 => s.active_gear_slot = 8,
1981                            // chest
1982                            12 => s.active_gear_slot = 9,
1983                            // hands/gloves
1984                            13 => s.active_gear_slot = 10,
1985                            // jewelry center
1986                            14 => s.active_gear_slot = 12,
1987                            // hat
1988                            15 => s.active_gear_slot = 14,
1989                            // tabard
1990                            16 => s.active_gear_slot = 19,
1991                            // glider
1992                            17 => s.active_gear_slot = 16,
1993                            // lantern
1994                            18 => s.active_gear_slot = 17,
1995                            // Bag 1
1996                            19 => s.active_gear_slot = 20,
1997                            // Bag 2
1998                            20 => s.active_gear_slot = 21,
1999                            // Bag 3
2000                            21 => s.active_gear_slot = 22,
2001                            // Bag 4
2002                            22 => s.is_focused = false,
2003                            // reset to 1 if unexpected
2004                            _ => s.active_gear_slot = 1,
2005                        }
2006                    }),
2007                    MenuInput::Left => state.update(|s| {
2008                        match s.active_gear_slot {
2009                            // weapon switch button
2010                            0 => {},
2011                            // primary weapon 1
2012                            1 => s.active_gear_slot = 0 + 1,
2013                            // secondary weapon 1
2014                            2 => s.active_gear_slot = 1,
2015                            // secondary weapon 2
2016                            3 => s.active_gear_slot = 2,
2017                            // primary weapon 2
2018                            4 => s.active_gear_slot = 3,
2019                            // back
2020                            5 => {},
2021                            // pants
2022                            6 => s.active_gear_slot = 5,
2023                            // shoes
2024                            7 => s.active_gear_slot = 6,
2025                            // jewelry left
2026                            8 => {},
2027                            // belt
2028                            9 => s.active_gear_slot = 8,
2029                            // jewelry right
2030                            10 => s.active_gear_slot = 9,
2031                            // shoulder
2032                            11 => {},
2033                            // chest
2034                            12 => s.active_gear_slot = 11,
2035                            // hands/gloves
2036                            13 => s.active_gear_slot = 12,
2037                            // jewelry center
2038                            14 => {},
2039                            // hat
2040                            15 => {},
2041                            // tabard
2042                            16 => s.active_gear_slot = 13,
2043                            // glider
2044                            17 => s.active_gear_slot = 14,
2045                            // lantern
2046                            18 => s.active_gear_slot = 15,
2047                            // Bag 1
2048                            19 => s.active_gear_slot = 13,
2049                            // Bag 2
2050                            20 => s.active_gear_slot = 10,
2051                            // Bag 3
2052                            21 => s.active_gear_slot = 7,
2053                            // Bag 4
2054                            22 => s.active_gear_slot = 4,
2055                            // reset to 1 if unexpected
2056                            _ => s.active_gear_slot = 1,
2057                        }
2058                    }),
2059                    MenuInput::Right => state.update(|s| {
2060                        match s.active_gear_slot {
2061                            // weapon switch button
2062                            0 => s.active_gear_slot = 1,
2063                            // primary weapon 1
2064                            1 => s.active_gear_slot = 2,
2065                            // secondary weapon 1
2066                            2 => s.active_gear_slot = 3,
2067                            // secondary weapon 2
2068                            3 => s.active_gear_slot = 4,
2069                            // primary weapon 1
2070                            4 => s.active_gear_slot = 22,
2071                            // back
2072                            5 => s.active_gear_slot = 6,
2073                            // pants
2074                            6 => s.active_gear_slot = 7,
2075                            // shoes
2076                            7 => s.active_gear_slot = 21,
2077                            // jewelry left
2078                            8 => s.active_gear_slot = 9,
2079                            // belt
2080                            9 => s.active_gear_slot = 10,
2081                            // jewelry right
2082                            10 => s.active_gear_slot = 20,
2083                            // shoulder
2084                            11 => s.active_gear_slot = 12,
2085                            // chest
2086                            12 => s.active_gear_slot = 13,
2087                            // hands/gloves
2088                            13 => s.active_gear_slot = 19,
2089                            // jewelry center
2090                            14 => s.active_gear_slot = 17,
2091                            // hat
2092                            15 => s.active_gear_slot = 18,
2093                            // tabard
2094                            16 => {},
2095                            // glider
2096                            17 => {},
2097                            // lantern
2098                            18 => {},
2099                            // Bag 1
2100                            19 => {},
2101                            // Bag 2
2102                            20 => {},
2103                            // Bag 3
2104                            21 => {},
2105                            // Bag 4
2106                            22 => {},
2107                            // reset to 1 if unexpected
2108                            _ => s.active_gear_slot = 1,
2109                        }
2110                    }),
2111                    MenuInput::Apply => {
2112                        // TODO
2113                    },
2114                    MenuInput::Back => events.push(GearMenuEvent::Close),
2115                    _ => {},
2116                }
2117            }
2118        }
2119
2120        // Inventory slots (filtered to equipment only)
2121        let grid_width = 376.0;
2122        let grid_height = 200.0;
2123
2124        Rectangle::fill_with([grid_width, grid_height], color::TRANSPARENT)
2125            .mid_bottom_with_margin_on(self.bag_ids.bg_frame, 3.5)
2126            .scroll_kids_vertically()
2127            .set(state.ids.inv_alignment, ui);
2128
2129        if !self.show.bag_menu_split || self.show.trade {
2130            // Adds spacing above the slots to make scrolling feel a bit more natural
2131            Rectangle::fill_with([grid_width, 5.0], color::TRANSPARENT)
2132                .mid_top_of(state.ids.inv_alignment)
2133                .set(state.ids.spacing_above, ui);
2134
2135            let mut space_max = 0;
2136            let compact_mode = self
2137                .tab_package
2138                .global_state
2139                .settings
2140                .interface
2141                .toggle_compact_item_slots;
2142            let columns = if compact_mode { 9 } else { 6 };
2143            let spacing = if compact_mode { 2.0 } else { 5.8 };
2144            let slot_size = if compact_mode { 40.0 } else { 57.8 };
2145            let scroll_size = if compact_mode { 36 } else { 24 };
2146
2147            // Bag slots
2148            for event in SlotGrid::new(
2149            self.tab_package.client,
2150            self.tab_package.global_state,
2151            self.tab_package.imgs,
2152            self.tab_package.item_imgs,
2153            self.tab_package.fonts,
2154            self.item_tooltip_manager,
2155            self.slot_manager,
2156            self.inventory,
2157            self.item_tooltip,
2158            self.tab_package.localized_strings,
2159            self.tab_package.item_i18n,
2160            self.tab_package.info.viewpoint_entity,
2161            self.tab_package.pulse,
2162            self.menu_events,
2163            true,                  // is_us
2164            self.show.bag_details, // details_mode
2165            self.show.crafting_fields.salvage,
2166        )
2167        .columns(columns)
2168        .spacing(if self.show.bag_details { 0.0 } else { spacing })
2169        // If the items are in focused, then gear is not (and vice-versa)
2170        .is_focused(!state.is_focused)
2171        .slot_size(if self.show.bag_details { 20.0 } else { slot_size })
2172        .filter(TabFilters::Gear)
2173        .w_of(state.ids.inv_alignment)
2174        .down_from(state.ids.spacing_above, 0.0)
2175        .set(state.ids.slot_grid, ui)
2176            {
2177                match event {
2178                    SlotEvents::Close => events.push(GearMenuEvent::Close),
2179                    SlotEvents::ExitUp => {
2180                        // User went up, out of the item list
2181                        // Switch focus to this widget---gear slots
2182                        state.update(|s| {
2183                            s.is_focused = true;
2184                        })
2185                    },
2186                    SlotEvents::FilteredSize(size) => {
2187                        space_max = size;
2188                    },
2189                    _ => {},
2190                }
2191            }
2192
2193            // Scrollbar
2194            if space_max > scroll_size {
2195                // Scrollbar-BG
2196                Image::new(self.tab_package.imgs.scrollbar_bg)
2197                    .w_h(9.0, 180.0)
2198                    .bottom_right_with_margins_on(self.bag_ids.bg_frame, 24.0, 3.0)
2199                    .color(Some(UI_HIGHLIGHT_0))
2200                    .set(state.ids.scrollbar_bg, ui);
2201                // Scrollbar
2202                Scrollbar::y_axis(state.ids.inv_alignment)
2203                    .thickness(6.0)
2204                    .h(133.0)
2205                    .color(UI_MAIN)
2206                    .middle_of(state.ids.scrollbar_bg)
2207                    .set(state.ids.scrollbar_slots, ui);
2208            };
2209        }
2210
2211        // A visual divider above the item grid
2212        Rectangle::fill_with([grid_width, 1.5], Color::Rgba(0.314, 0.443, 0.443, 1.0))
2213            .up_from(state.ids.inv_alignment, 0.0)
2214            .set(state.ids.scroll_divider, ui);
2215
2216        // Armor slots
2217        let mut slot_maker = SlotMaker {
2218            empty_slot: self.tab_package.imgs.armor_slot_empty,
2219            hovered_slot: self.tab_package.imgs.skillbar_index,
2220            filled_slot: self.tab_package.imgs.armor_slot,
2221            selected_slot: self.tab_package.imgs.armor_slot_sel,
2222            background_color: Some(UI_HIGHLIGHT_0),
2223            content_size: ContentSize {
2224                width_height_ratio: 1.0,
2225                max_fraction: 0.75, /* Changes the item image size by setting a maximum
2226                                     * fraction
2227                                     * of either the width or height */
2228            },
2229            selected_content_scale: 1.067,
2230            amount_font: self.tab_package.fonts.cyri.conrod_id,
2231            amount_margins: Vec2::new(-4.0, 0.0),
2232            amount_font_size: self.tab_package.fonts.cyri.scale(12),
2233            amount_text_color: TEXT_COLOR,
2234            content_source: self.inventory,
2235            image_source: self.tab_package.item_imgs,
2236            slot_manager: Some(self.slot_manager),
2237            global_state: self.tab_package.global_state,
2238            pulse: self.tab_package.pulse,
2239        };
2240
2241        // NOTE: Yes, macros considered harmful.
2242        // Though, this code mutably captures two different fields of `self`
2243        // This works because it's different branches of if-let
2244        // so in reality borrow checker allows you to do this as you
2245        // capture only one field.
2246        //
2247        // The less impossible, but still tricky part is denote type of
2248        // `$slot_maker` which has 1 lifetype parameter and 3 type parameters
2249        // in such way that it implements all traits conrod needs.
2250        //
2251        // And final part is that this uses that much of arguments
2252        // that just by passing all of them, you will get about the same
2253        // amount of lines this macro has or even more.
2254        //
2255        // So considering how many times we copy-paste this code
2256        // and how easy this macro looks it sounds like lawful evil.
2257        //
2258        // What this actually does is checks if we have equipped item on this slot
2259        // and if we do, display item tooltip for it.
2260        // If not, just show text of slot name.
2261        macro_rules! set_tooltip {
2262            ($slot_maker:expr, $slot_id:expr, $slot:expr, $desc:expr) => {
2263                if let Some(item) = self.inventory.equipped($slot) {
2264                    let manager = &mut *self.item_tooltip_manager;
2265                    $slot_maker
2266                        .with_item_tooltip(
2267                            manager,
2268                            core::iter::once(item as &dyn ItemDesc),
2269                            &None,
2270                            self.item_tooltip,
2271                        )
2272                        .set($slot_id, ui)
2273                } else {
2274                    let manager = &mut *self.tooltip_manager;
2275                    $slot_maker
2276                        .with_tooltip(
2277                            manager,
2278                            &i18n.get_msg($desc),
2279                            "",
2280                            self.tooltip,
2281                            color::WHITE,
2282                        )
2283                        .set($slot_id, ui)
2284                }
2285            };
2286        }
2287
2288        let filled_slot = self.tab_package.imgs.armor_slot;
2289        // Stat icons and text
2290        state.update(|s| {
2291            s.ids
2292                .stat_icons
2293                .resize(STATS.len(), &mut ui.widget_id_generator())
2294        });
2295        state.update(|s| {
2296            s.ids
2297                .stat_txts
2298                .resize(STATS.len(), &mut ui.widget_id_generator())
2299        });
2300        // Stats
2301        let block_gear_tab = self.show.bag_menu_split && !self.show.trade;
2302        let combat_rating = combat_rating(
2303            self.inventory,
2304            self.health,
2305            self.energy,
2306            self.poise,
2307            self.skill_set,
2308            *self.body,
2309            self.msm,
2310        )
2311        .min(999.9);
2312        let indicator_col = cr_color(combat_rating);
2313        let img_size = if block_gear_tab { 25.0 } else { 20.0 };
2314        for i in STATS.iter().copied().enumerate() {
2315            let btn = Button::image(match i.1 {
2316                "Health" => self.tab_package.imgs.health_ico,
2317                "Energy" => self.tab_package.imgs.energy_ico,
2318                "Combat Rating" => self.tab_package.imgs.combat_rating_ico,
2319                "Protection" => self.tab_package.imgs.protection_ico,
2320                "Stun Resilience" => self.tab_package.imgs.stun_res_ico,
2321                "Stealth" => self.tab_package.imgs.stealth_rating_ico,
2322                _ => self.tab_package.imgs.nothing,
2323            })
2324            .w_h(img_size, img_size)
2325            .image_color(if i.1 == "Combat Rating" {
2326                indicator_col
2327            } else {
2328                TEXT_COLOR
2329            });
2330            let protection_txt = format!(
2331                "{}%",
2332                (100.0
2333                    * Damage::compute_damage_reduction(
2334                        None,
2335                        Some(self.inventory),
2336                        Some(self.stats),
2337                        self.msm
2338                    )) as i32
2339            );
2340            let health_txt = format!("{}", self.health.maximum().round() as usize);
2341            let energy_txt = format!("{}", self.energy.maximum().round() as usize);
2342            let combat_rating_txt = format!("{}", (combat_rating * 10.0) as usize);
2343            let stun_res_txt = format!(
2344                "{}",
2345                (100.0
2346                    * Poise::compute_poise_damage_reduction(
2347                        Some(self.inventory),
2348                        self.msm,
2349                        None,
2350                        Some(self.stats),
2351                    )) as i32
2352            );
2353            let stealth_txt = format!(
2354                "{:.1}%",
2355                ((1.0
2356                    - perception_dist_multiplier_from_stealth(
2357                        Some(self.inventory),
2358                        None,
2359                        self.msm
2360                    ))
2361                    * 100.0)
2362            );
2363            let btn = if i.0 == 0 {
2364                if !self.show.bag_menu_split || self.show.trade {
2365                    // Top left corner of gear tab
2366                    btn.top_left_with_margins_on(self.bag_ids.bg_frame, 95.0, 10.0)
2367                } else {
2368                    // Bottom of gear tab
2369                    btn.top_left_with_margins_on(state.ids.inv_alignment, 10.0, 25.0)
2370                }
2371            } else {
2372                if !self.show.bag_menu_split || self.show.trade {
2373                    // Top left corner of gear tab
2374                    btn.down_from(state.ids.stat_icons[i.0 - 1], 7.0)
2375                } else {
2376                    // Bottom of gear tab
2377                    if i.0 % 2 != 0 {
2378                        // If odd, place beneath previous stat
2379                        btn.down_from(state.ids.stat_icons[i.0 - 1], 15.0)
2380                    } else {
2381                        // If even
2382                        btn.right_from(state.ids.stat_icons[i.0 - 2], 90.0)
2383                    }
2384                }
2385            };
2386            let tooltip_head = match i.1 {
2387                "Health" => i18n.get_msg("hud-bag-health"),
2388                "Energy" => i18n.get_msg("hud-bag-energy"),
2389                "Combat Rating" => i18n.get_msg("hud-bag-combat_rating"),
2390                "Protection" => i18n.get_msg("hud-bag-protection"),
2391                "Stun Resilience" => i18n.get_msg("hud-bag-stun_res"),
2392                "Stealth" => i18n.get_msg("hud-bag-stealth"),
2393                _ => Cow::Borrowed(""),
2394            };
2395            let tooltip_txt = match i.1 {
2396                "Combat Rating" => i18n.get_msg("hud-bag-combat_rating_desc"),
2397                "Protection" => i18n.get_msg("hud-bag-protection_desc"),
2398                "Stun Resilience" => i18n.get_msg("hud-bag-stun_res_desc"),
2399                _ => Cow::Borrowed(""),
2400            };
2401            btn.with_tooltip(
2402                self.tooltip_manager,
2403                &tooltip_head,
2404                &tooltip_txt,
2405                self.bag_tooltip,
2406                TEXT_COLOR,
2407            )
2408            .set(state.ids.stat_icons[i.0], ui);
2409            Text::new(match i.1 {
2410                "Health" => &health_txt,
2411                "Energy" => &energy_txt,
2412                "Combat Rating" => &combat_rating_txt,
2413                "Protection" => &protection_txt,
2414                "Stun Resilience" => &stun_res_txt,
2415                "Stealth" => &stealth_txt,
2416                _ => "",
2417            })
2418            .right_from(state.ids.stat_icons[i.0], 10.0)
2419            .font_id(self.tab_package.fonts.cyri.conrod_id)
2420            .font_size(if block_gear_tab {
2421                self.tab_package.fonts.cyri.scale(20)
2422            } else {
2423                self.tab_package.fonts.cyri.scale(14)
2424            })
2425            .color(TEXT_COLOR)
2426            .graphics_for(state.ids.stat_icons[i.0])
2427            .set(state.ids.stat_txts[i.0], ui);
2428        }
2429        // Loadout Slots
2430        // Head
2431        let item_slot = EquipSlot::Armor(ArmorSlot::Head);
2432        let slot = slot_maker
2433            .fabricate(
2434                item_slot,
2435                [40.0; 2],
2436                state.active_gear_slot == 15 && state.is_focused,
2437                false,
2438            )
2439            .mid_top_with_margin_on(self.bag_ids.bg_frame, 100.0)
2440            .with_icon(
2441                self.tab_package.imgs.head_bg,
2442                Vec2::new(32.0, 40.0),
2443                Some(UI_MAIN),
2444            )
2445            .filled_slot(filled_slot);
2446
2447        let slot_id = state.ids.head_slot;
2448        set_tooltip!(slot, slot_id, item_slot, "hud-bag-head");
2449
2450        // Necklace
2451        let item_slot = EquipSlot::Armor(ArmorSlot::Neck);
2452        let slot = slot_maker
2453            .fabricate(
2454                item_slot,
2455                [40.0; 2],
2456                state.active_gear_slot == 14 && state.is_focused,
2457                false,
2458            )
2459            .mid_bottom_with_margin_on(state.ids.head_slot, -50.0)
2460            .with_icon(
2461                self.tab_package.imgs.necklace_bg,
2462                Vec2::new(40.0, 31.0),
2463                Some(UI_MAIN),
2464            )
2465            .filled_slot(filled_slot);
2466
2467        let slot_id = state.ids.neck_slot;
2468        set_tooltip!(slot, slot_id, item_slot, "hud-bag-neck");
2469
2470        // Chest
2471        //Image::new(self.imgs.armor_slot) // different graphics for empty/non empty
2472        let item_slot = EquipSlot::Armor(ArmorSlot::Chest);
2473        let slot = slot_maker
2474            .fabricate(
2475                item_slot,
2476                [80.0; 2],
2477                state.active_gear_slot == 12 && state.is_focused,
2478                false,
2479            )
2480            .mid_bottom_with_margin_on(state.ids.neck_slot, -90.0)
2481            .with_icon(
2482                self.tab_package.imgs.chest_bg,
2483                Vec2::new(64.0, 42.0),
2484                Some(UI_MAIN),
2485            )
2486            .filled_slot(filled_slot);
2487
2488        let slot_id = state.ids.chest_slot;
2489        set_tooltip!(slot, slot_id, item_slot, "hud-bag-chest");
2490
2491        // Shoulders
2492        let item_slot = EquipSlot::Armor(ArmorSlot::Shoulders);
2493        let slot = slot_maker
2494            .fabricate(
2495                item_slot,
2496                [65.0; 2],
2497                state.active_gear_slot == 11 && state.is_focused,
2498                false,
2499            )
2500            .bottom_left_with_margins_on(state.ids.chest_slot, 0.0, -80.0)
2501            .with_icon(
2502                self.tab_package.imgs.shoulders_bg,
2503                Vec2::new(60.0, 36.0),
2504                Some(UI_MAIN),
2505            )
2506            .filled_slot(filled_slot);
2507
2508        let slot_id = state.ids.shoulders_slot;
2509        set_tooltip!(slot, slot_id, item_slot, "hud-bag-shoulders");
2510
2511        // Hands
2512        let item_slot = EquipSlot::Armor(ArmorSlot::Hands);
2513        let slot = slot_maker
2514            .fabricate(
2515                item_slot,
2516                [65.0; 2],
2517                state.active_gear_slot == 13 && state.is_focused,
2518                false,
2519            )
2520            .bottom_right_with_margins_on(state.ids.chest_slot, 0.0, -80.0)
2521            .with_icon(
2522                self.tab_package.imgs.hands_bg,
2523                Vec2::new(55.0, 60.0),
2524                Some(UI_MAIN),
2525            )
2526            .filled_slot(filled_slot);
2527
2528        let slot_id = state.ids.hands_slot;
2529        set_tooltip!(slot, slot_id, item_slot, "hud-bag-hands");
2530
2531        // Belt
2532        let item_slot = EquipSlot::Armor(ArmorSlot::Belt);
2533        let slot = slot_maker
2534            .fabricate(
2535                item_slot,
2536                [40.0; 2],
2537                state.active_gear_slot == 9 && state.is_focused,
2538                false,
2539            )
2540            .mid_bottom_with_margin_on(state.ids.chest_slot, -50.0)
2541            .with_icon(
2542                self.tab_package.imgs.belt_bg,
2543                Vec2::new(40.0, 23.0),
2544                Some(UI_MAIN),
2545            )
2546            .filled_slot(filled_slot);
2547
2548        let slot_id = state.ids.belt_slot;
2549        set_tooltip!(slot, slot_id, item_slot, "hud-bag-belt");
2550
2551        // Legs
2552        let item_slot = EquipSlot::Armor(ArmorSlot::Legs);
2553        let slot = slot_maker
2554            .fabricate(
2555                item_slot,
2556                [80.0; 2],
2557                state.active_gear_slot == 6 && state.is_focused,
2558                false,
2559            )
2560            .mid_bottom_with_margin_on(state.ids.belt_slot, -90.0)
2561            .with_icon(
2562                self.tab_package.imgs.legs_bg,
2563                Vec2::new(48.0, 70.0),
2564                Some(UI_MAIN),
2565            )
2566            .filled_slot(filled_slot);
2567
2568        let slot_id = state.ids.legs_slot;
2569        set_tooltip!(slot, slot_id, item_slot, "hud-bag-legs");
2570
2571        // Ring right
2572        let item_slot = EquipSlot::Armor(ArmorSlot::Ring1);
2573        let slot = slot_maker
2574            .fabricate(
2575                item_slot,
2576                [40.0; 2],
2577                state.active_gear_slot == 10 && state.is_focused,
2578                false,
2579            )
2580            .bottom_left_with_margins_on(state.ids.hands_slot, -50.0, 0.0)
2581            .with_icon(
2582                self.tab_package.imgs.ring_bg,
2583                Vec2::new(36.0, 40.0),
2584                Some(UI_MAIN),
2585            )
2586            .filled_slot(filled_slot);
2587
2588        let slot_id = state.ids.ring1_slot;
2589        set_tooltip!(slot, slot_id, item_slot, "hud-bag-ring");
2590
2591        // Ring left
2592        let item_slot = EquipSlot::Armor(ArmorSlot::Ring2);
2593        let slot = slot_maker
2594            .fabricate(
2595                item_slot,
2596                [40.0; 2],
2597                state.active_gear_slot == 8 && state.is_focused,
2598                false,
2599            )
2600            .bottom_right_with_margins_on(state.ids.shoulders_slot, -50.0, 0.0)
2601            .with_icon(
2602                self.tab_package.imgs.ring_bg,
2603                Vec2::new(36.0, 40.0),
2604                Some(UI_MAIN),
2605            )
2606            .filled_slot(filled_slot);
2607
2608        let slot_id = state.ids.ring2_slot;
2609        set_tooltip!(slot, slot_id, item_slot, "hud-bag-ring");
2610
2611        // Back
2612        let item_slot = EquipSlot::Armor(ArmorSlot::Back);
2613        let slot = slot_maker
2614            .fabricate(
2615                item_slot,
2616                [40.0; 2],
2617                state.active_gear_slot == 5 && state.is_focused,
2618                false,
2619            )
2620            .down_from(state.ids.ring2_slot, 10.0)
2621            .with_icon(
2622                self.tab_package.imgs.back_bg,
2623                Vec2::new(33.0, 40.0),
2624                Some(UI_MAIN),
2625            )
2626            .filled_slot(filled_slot);
2627
2628        let slot_id = state.ids.back_slot;
2629        set_tooltip!(slot, slot_id, item_slot, "hud-bag-back");
2630
2631        // Foot
2632        let item_slot = EquipSlot::Armor(ArmorSlot::Feet);
2633        let slot = slot_maker
2634            .fabricate(
2635                item_slot,
2636                [40.0; 2],
2637                state.active_gear_slot == 7 && state.is_focused,
2638                false,
2639            )
2640            .down_from(state.ids.ring1_slot, 10.0)
2641            .with_icon(
2642                self.tab_package.imgs.feet_bg,
2643                Vec2::new(32.0, 40.0),
2644                Some(UI_MAIN),
2645            )
2646            .filled_slot(filled_slot);
2647
2648        let slot_id = state.ids.feet_slot;
2649        set_tooltip!(slot, slot_id, item_slot, "hud-bag-feet");
2650
2651        // Lantern
2652        let item_slot = EquipSlot::Lantern;
2653        let slot = slot_maker
2654            .fabricate(
2655                item_slot,
2656                [40.0; 2],
2657                state.active_gear_slot == 18 && state.is_focused,
2658                false,
2659            )
2660            .top_right_with_margins_on(self.bag_ids.bg_frame, 100.0, 5.0)
2661            .with_icon(
2662                self.tab_package.imgs.lantern_bg,
2663                Vec2::new(24.0, 38.0),
2664                Some(UI_MAIN),
2665            )
2666            .filled_slot(filled_slot);
2667
2668        let slot_id = state.ids.lantern_slot;
2669        set_tooltip!(slot, slot_id, item_slot, "hud-bag-lantern");
2670
2671        // Glider
2672        let item_slot = EquipSlot::Glider;
2673        let slot = slot_maker
2674            .fabricate(
2675                item_slot,
2676                [40.0; 2],
2677                state.active_gear_slot == 17 && state.is_focused,
2678                false,
2679            )
2680            .down_from(state.ids.lantern_slot, 5.0)
2681            .with_icon(
2682                self.tab_package.imgs.glider_bg,
2683                Vec2::new(38.0, 38.0),
2684                Some(UI_MAIN),
2685            )
2686            .filled_slot(filled_slot);
2687
2688        let slot_id = state.ids.glider_slot;
2689        set_tooltip!(slot, slot_id, item_slot, "hud-bag-glider");
2690
2691        // Tabard
2692        let item_slot = EquipSlot::Armor(ArmorSlot::Tabard);
2693        let slot = slot_maker
2694            .fabricate(
2695                item_slot,
2696                [40.0; 2],
2697                state.active_gear_slot == 16 && state.is_focused,
2698                false,
2699            )
2700            .down_from(state.ids.glider_slot, 5.0)
2701            .with_icon(
2702                self.tab_package.imgs.tabard_bg,
2703                Vec2::new(38.0, 38.0),
2704                Some(UI_MAIN),
2705            )
2706            .filled_slot(filled_slot);
2707
2708        let slot_id = state.ids.tabard_slot;
2709        set_tooltip!(slot, slot_id, item_slot, "hud-bag-tabard");
2710
2711        // Active Mainhand/Left-Slot
2712        let item_slot = EquipSlot::ActiveMainhand;
2713        let slot = slot_maker
2714            .fabricate(
2715                item_slot,
2716                [80.0; 2],
2717                state.active_gear_slot == 1 && state.is_focused,
2718                false,
2719            )
2720            .bottom_right_with_margins_on(state.ids.back_slot, -90.0, 0.0)
2721            .with_icon(
2722                self.tab_package.imgs.mainhand_bg,
2723                Vec2::new(75.0, 75.0),
2724                Some(UI_MAIN),
2725            )
2726            .filled_slot(filled_slot);
2727
2728        let slot_id = state.ids.active_mainhand_slot;
2729        set_tooltip!(slot, slot_id, item_slot, "hud-bag-mainhand");
2730
2731        // Active Offhand/Right-Slot
2732        let item_slot = EquipSlot::ActiveOffhand;
2733        let slot = slot_maker
2734            .fabricate(
2735                item_slot,
2736                [80.0; 2],
2737                state.active_gear_slot == 4 && state.is_focused,
2738                false,
2739            )
2740            .bottom_left_with_margins_on(state.ids.feet_slot, -90.0, 0.0)
2741            .with_icon(
2742                self.tab_package.imgs.offhand_bg,
2743                Vec2::new(75.0, 75.0),
2744                Some(UI_MAIN),
2745            )
2746            .filled_slot(filled_slot);
2747
2748        let slot_id = state.ids.active_offhand_slot;
2749        set_tooltip!(slot, slot_id, item_slot, "hud-bag-offhand");
2750
2751        // Inactive Mainhand/Left-Slot
2752        let item_slot = EquipSlot::InactiveMainhand;
2753        let slot = slot_maker
2754            .fabricate(
2755                item_slot,
2756                [35.0; 2],
2757                state.active_gear_slot == 2 && state.is_focused,
2758                false,
2759            )
2760            .bottom_right_with_margins_on(state.ids.active_mainhand_slot, 0.0, -47.0)
2761            .with_icon(
2762                self.tab_package.imgs.mainhand_bg,
2763                Vec2::new(35.0, 35.0),
2764                Some(UI_MAIN),
2765            )
2766            .filled_slot(filled_slot);
2767
2768        let slot_id = state.ids.inactive_mainhand_slot;
2769        set_tooltip!(slot, slot_id, item_slot, "hud-bag-inactive_mainhand");
2770
2771        // Inactive Offhand/Right-Slot
2772        let item_slot = EquipSlot::InactiveOffhand;
2773        let slot = slot_maker
2774            .fabricate(
2775                item_slot,
2776                [35.0; 2],
2777                state.active_gear_slot == 3 && state.is_focused,
2778                false,
2779            )
2780            .bottom_left_with_margins_on(state.ids.active_offhand_slot, 0.0, -47.0)
2781            .with_icon(
2782                self.tab_package.imgs.offhand_bg,
2783                Vec2::new(35.0, 35.0),
2784                Some(UI_MAIN),
2785            )
2786            .filled_slot(filled_slot);
2787
2788        let slot_id = state.ids.inactive_offhand_slot;
2789        set_tooltip!(slot, slot_id, item_slot, "hud-bag-inactive_offhand");
2790
2791        if Button::image(self.tab_package.imgs.swap_equipped_weapons_btn)
2792            .hover_image(self.tab_package.imgs.swap_equipped_weapons_btn_hover)
2793            .press_image(self.tab_package.imgs.swap_equipped_weapons_btn_press)
2794            .w_h(32.0, 40.0)
2795            .bottom_left_with_margins_on(self.bag_ids.bg_frame, 0.0, 23.3)
2796            .align_middle_y_of(state.ids.active_mainhand_slot)
2797            .with_tooltip(
2798                self.tooltip_manager,
2799                &i18n.get_msg("hud-bag-swap_equipped_weapons_title"),
2800                &(if let Some(key) = self
2801                    .tab_package
2802                    .global_state
2803                    .settings
2804                    .controls
2805                    .get_binding(GameInput::SwapLoadout)
2806                {
2807                    i18n.get_msg_ctx("hud-bag-swap_equipped_weapons_desc", &i18n::fluent_args! {
2808                        "key" => key.display_string()
2809                    })
2810                } else {
2811                    Cow::Borrowed("")
2812                }),
2813                self.tooltip,
2814                color::WHITE,
2815            )
2816            .set(state.ids.swap_equipped_weapons_btn, ui)
2817            .was_clicked()
2818        {
2819            events.push(GearMenuEvent::SwapEquippedWeapons);
2820        }
2821
2822        // Bag 1
2823        let item_slot = EquipSlot::Armor(ArmorSlot::Bag1);
2824        let slot = slot_maker
2825            .fabricate(
2826                item_slot,
2827                [40.0; 2],
2828                state.active_gear_slot == 19 && state.is_focused,
2829                false,
2830            )
2831            .down_from(state.ids.tabard_slot, 25.0)
2832            .with_icon(
2833                self.tab_package.imgs.bag_bg,
2834                Vec2::new(28.0, 24.0),
2835                Some(UI_MAIN),
2836            )
2837            .filled_slot(filled_slot);
2838
2839        let slot_id = state.ids.bag1_slot;
2840        set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
2841
2842        // Bag 2
2843        let item_slot = EquipSlot::Armor(ArmorSlot::Bag2);
2844        let slot = slot_maker
2845            .fabricate(
2846                item_slot,
2847                [40.0; 2],
2848                state.active_gear_slot == 20 && state.is_focused,
2849                false,
2850            )
2851            .down_from(state.ids.bag1_slot, 5.0)
2852            .with_icon(
2853                self.tab_package.imgs.bag_bg,
2854                Vec2::new(28.0, 24.0),
2855                Some(UI_MAIN),
2856            )
2857            .filled_slot(filled_slot);
2858
2859        let slot_id = state.ids.bag2_slot;
2860        set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
2861
2862        // Bag 3
2863        let item_slot = EquipSlot::Armor(ArmorSlot::Bag3);
2864        let slot = slot_maker
2865            .fabricate(
2866                item_slot,
2867                [40.0; 2],
2868                state.active_gear_slot == 21 && state.is_focused,
2869                false,
2870            )
2871            .down_from(state.ids.bag2_slot, 5.0)
2872            .with_icon(
2873                self.tab_package.imgs.bag_bg,
2874                Vec2::new(28.0, 24.0),
2875                Some(UI_MAIN),
2876            )
2877            .filled_slot(filled_slot);
2878
2879        let slot_id = state.ids.bag3_slot;
2880        set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
2881
2882        // Bag 4
2883        let item_slot = EquipSlot::Armor(ArmorSlot::Bag4);
2884        let slot = slot_maker
2885            .fabricate(
2886                item_slot,
2887                [40.0; 2],
2888                state.active_gear_slot == 22 && state.is_focused,
2889                false,
2890            )
2891            .down_from(state.ids.bag3_slot, 2.0)
2892            .with_icon(
2893                self.tab_package.imgs.bag_bg,
2894                Vec2::new(28.0, 24.0),
2895                Some(UI_MAIN),
2896            )
2897            .filled_slot(filled_slot);
2898
2899        let slot_id = state.ids.bag4_slot;
2900        set_tooltip!(slot, slot_id, item_slot, "hud-bag-bag");
2901
2902        events
2903    }
2904}