Skip to main content

veloren_voxygen/hud/
crafting.rs

1use super::{
2    HudInfo, Show, TEXT_COLOR, TEXT_DULL_RED_COLOR, TEXT_GRAY_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
3    get_quality_col,
4    img_ids::{Imgs, ImgsRot},
5    item_imgs::{ItemImgs, animate_by_pulse},
6    slots::{CraftSlot, CraftSlotInfo, SlotManager},
7    util,
8};
9use crate::{
10    GlobalState,
11    settings::{HudPositionSettings, Settings},
12    ui::{
13        ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable, Tooltip, TooltipManager,
14        Tooltipable,
15        fonts::Fonts,
16        slot::{ContentSize, SlotMaker},
17    },
18};
19use client::{self, Client};
20use common::{
21    assets::AssetExt,
22    comp::inventory::{
23        Inventory,
24        item::{
25            Item, ItemBase, ItemDef, ItemDesc, ItemI18n, ItemKind, ItemTag, MaterialStatManifest,
26            Quality, TagExampleInfo,
27            item_key::ItemKey,
28            modular::{self, ModularComponent},
29            tool::{AbilityMap, ToolKind},
30        },
31        slot::{InvSlotId, Slot},
32    },
33    mounting::VolumePos,
34    recipe::{ComponentKey, Recipe, RecipeBookManifest, RecipeInput},
35    terrain::SpriteKind,
36};
37use conrod_core::{
38    Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, color, image,
39    position::{Dimension, Place},
40    widget::{self, Button, Image, Line, Rectangle, Scrollbar, Text, TextEdit},
41    widget_ids,
42};
43use hashbrown::{HashMap, HashSet};
44use i18n::Localization;
45use itertools::Either;
46use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
47use strum::{EnumIter, IntoEnumIterator};
48use tracing::{error, warn};
49use vek::{approx::AbsDiffEq, *};
50
51widget_ids! {
52    pub struct Ids {
53        window,
54        window_frame,
55        close,
56        icon,
57        title_main,
58        title_rec,
59        align_rec,
60        scrollbar_rec,
61        btn_show_all_recipes,
62        btn_open_search,
63        btn_close_search,
64        input_search,
65        input_bg_search,
66        input_overlay_search,
67        title_ing,
68        tags_ing[],
69        align_ing,
70        scrollbar_ing,
71        btn_separator,
72        btn_craft,
73        btn_craft_all,
74        recipe_list_btns[],
75        recipe_list_labels[],
76        recipe_list_quality_indicators[],
77        recipe_list_materials_indicators[],
78        ingredients[],
79        ingredient_frame[],
80        ingredient_btn[],
81        ingredient_img[],
82        req_text[],
83        ingredients_txt,
84        req_station_title,
85        req_station_img,
86        req_station_txt,
87        output_img_frame,
88        output_img,
89        category_bgs[],
90        category_tabs[],
91        category_imgs[],
92        dismantle_title,
93        dismantle_img,
94        dismantle_txt,
95        repair_buttons[],
96        craft_slots[],
97        modular_art,
98        modular_desc_txt,
99        modular_wep_empty_bg,
100        modular_wep_ing_1_bg,
101        modular_wep_ing_2_bg,
102        draggable_area,
103    }
104}
105
106pub enum Event {
107    CraftRecipe {
108        recipe_name: String,
109        amount: u32,
110    },
111    CraftModularWeapon {
112        primary_slot: InvSlotId,
113        secondary_slot: InvSlotId,
114    },
115    CraftModularWeaponComponent {
116        toolkind: ToolKind,
117        material: InvSlotId,
118        modifier: Option<InvSlotId>,
119    },
120    ChangeCraftingTab(CraftingTab),
121    Close,
122    Focus(widget::Id),
123    SearchRecipe(Option<String>),
124    ShowAllRecipes(bool),
125    ClearRecipeInputs,
126    RepairItem {
127        slot: Slot,
128    },
129    MoveCrafting(Vec2<f64>),
130}
131
132pub struct CraftingShow {
133    pub crafting_tab: CraftingTab,
134    pub crafting_search_key: Option<String>,
135    pub craft_sprite: Option<(VolumePos, SpriteKind)>,
136    pub salvage: bool,
137    pub initialize_repair: bool,
138    // TODO: Maybe try to do something that doesn't need to allocate?
139    pub recipe_inputs: HashMap<u32, Slot>,
140}
141
142impl Default for CraftingShow {
143    fn default() -> Self {
144        Self {
145            crafting_tab: CraftingTab::All,
146            crafting_search_key: None,
147            craft_sprite: None,
148            salvage: false,
149            initialize_repair: false,
150            recipe_inputs: HashMap::new(),
151        }
152    }
153}
154
155#[derive(WidgetCommon)]
156pub struct Crafting<'a> {
157    client: &'a Client,
158    global_state: &'a GlobalState,
159    info: &'a HudInfo<'a>,
160    imgs: &'a Imgs,
161    fonts: &'a Fonts,
162    localized_strings: &'a Localization,
163    item_i18n: &'a ItemI18n,
164    pulse: f32,
165    rot_imgs: &'a ImgsRot,
166    item_tooltip_manager: &'a mut ItemTooltipManager,
167    slot_manager: &'a mut SlotManager,
168    item_imgs: &'a ItemImgs,
169    inventory: &'a Inventory,
170    rbm: &'a RecipeBookManifest,
171    msm: &'a MaterialStatManifest,
172    #[conrod(common_builder)]
173    common: widget::CommonBuilder,
174    tooltip_manager: &'a mut TooltipManager,
175    show: &'a mut Show,
176    settings: &'a Settings,
177}
178
179impl<'a> Crafting<'a> {
180    #[expect(clippy::too_many_arguments)]
181    pub fn new(
182        client: &'a Client,
183        global_state: &'a GlobalState,
184        info: &'a HudInfo,
185        imgs: &'a Imgs,
186        fonts: &'a Fonts,
187        localized_strings: &'a Localization,
188        item_i18n: &'a ItemI18n,
189        pulse: f32,
190        rot_imgs: &'a ImgsRot,
191        item_tooltip_manager: &'a mut ItemTooltipManager,
192        slot_manager: &'a mut SlotManager,
193        item_imgs: &'a ItemImgs,
194        inventory: &'a Inventory,
195        rbm: &'a RecipeBookManifest,
196        msm: &'a MaterialStatManifest,
197        tooltip_manager: &'a mut TooltipManager,
198        show: &'a mut Show,
199        settings: &'a Settings,
200    ) -> Self {
201        Self {
202            client,
203            global_state,
204            info,
205            imgs,
206            fonts,
207            localized_strings,
208            item_i18n,
209            pulse,
210            rot_imgs,
211            item_tooltip_manager,
212            slot_manager,
213            tooltip_manager,
214            item_imgs,
215            inventory,
216            rbm,
217            msm,
218            show,
219            common: widget::CommonBuilder::default(),
220            settings,
221        }
222    }
223}
224
225#[derive(Copy, Clone, Debug, EnumIter, PartialEq, Eq)]
226pub enum CraftingTab {
227    All,
228    Tool,
229    Armor,
230    Weapon,
231    ProcessedMaterial,
232    Food,
233    Potion,
234    Bag,
235    Utility,
236    Glider,
237    Dismantle,
238}
239
240impl CraftingTab {
241    fn name_key(self) -> &'static str {
242        match self {
243            CraftingTab::All => "hud-crafting-tabs-all",
244            CraftingTab::Armor => "hud-crafting-tabs-armor",
245            CraftingTab::Food => "hud-crafting-tabs-food",
246            CraftingTab::Glider => "hud-crafting-tabs-glider",
247            CraftingTab::Potion => "hud-crafting-tabs-potion",
248            CraftingTab::Tool => "hud-crafting-tabs-tool",
249            CraftingTab::Utility => "hud-crafting-tabs-utility",
250            CraftingTab::Weapon => "hud-crafting-tabs-weapon",
251            CraftingTab::Bag => "hud-crafting-tabs-bag",
252            CraftingTab::ProcessedMaterial => "hud-crafting-tabs-processed_material",
253            CraftingTab::Dismantle => "hud-crafting-tabs-dismantle",
254        }
255    }
256
257    fn img_id(self, imgs: &Imgs) -> image::Id {
258        match self {
259            CraftingTab::All => imgs.icon_globe,
260            CraftingTab::Armor => imgs.icon_armor,
261            CraftingTab::Food => imgs.icon_food,
262            CraftingTab::Glider => imgs.icon_glider,
263            CraftingTab::Potion => imgs.icon_potion,
264            CraftingTab::Tool => imgs.icon_tools,
265            CraftingTab::Utility => imgs.icon_utility,
266            CraftingTab::Weapon => imgs.icon_weapon,
267            CraftingTab::Bag => imgs.icon_bag,
268            CraftingTab::ProcessedMaterial => imgs.icon_processed_material,
269            // These tabs are never shown, so using not found is fine
270            CraftingTab::Dismantle => imgs.not_found,
271        }
272    }
273
274    fn satisfies(self, recipe: &Recipe) -> bool {
275        let (item, _count) = &recipe.output;
276        match self {
277            CraftingTab::All | CraftingTab::Dismantle => true,
278            CraftingTab::Food => item.tags().contains(&ItemTag::Food),
279            CraftingTab::Armor => match &*item.kind() {
280                ItemKind::Armor(_) => !item.tags().contains(&ItemTag::Bag),
281                _ => false,
282            },
283            CraftingTab::Glider => matches!(&*item.kind(), ItemKind::Glider),
284            CraftingTab::Potion => item
285                .tags()
286                .iter()
287                .any(|tag| matches!(tag, &ItemTag::Potion | &ItemTag::Charm)),
288            CraftingTab::ProcessedMaterial => item
289                .tags()
290                .iter()
291                .any(|tag| matches!(tag, &ItemTag::MaterialKind(_) | &ItemTag::BaseMaterial)),
292            CraftingTab::Bag => item.tags().contains(&ItemTag::Bag),
293            CraftingTab::Tool => item.tags().contains(&ItemTag::CraftingTool),
294            CraftingTab::Utility => item.tags().contains(&ItemTag::Utility),
295            CraftingTab::Weapon => match &*item.kind() {
296                ItemKind::Tool(_) => !item.tags().contains(&ItemTag::CraftingTool),
297                ItemKind::ModularComponent(
298                    ModularComponent::ToolPrimaryComponent { .. }
299                    | ModularComponent::ToolSecondaryComponent { .. },
300                ) => true,
301                _ => false,
302            },
303        }
304    }
305
306    // Tells UI whether tab is an adhoc tab that should only sometimes be present
307    // depending on what station is accessed
308    fn is_adhoc(self) -> bool { matches!(self, CraftingTab::Dismantle) }
309}
310
311pub struct State {
312    ids: Ids,
313    selected_recipe: Option<String>,
314}
315
316enum SearchFilter {
317    None,
318    Input,
319    Nonexistent,
320}
321
322impl SearchFilter {
323    fn parse_from_str(string: &str) -> Self {
324        match string {
325            "input" => Self::Input,
326            _ => Self::Nonexistent,
327        }
328    }
329}
330
331impl Widget for Crafting<'_> {
332    type Event = Vec<Event>;
333    type State = State;
334    type Style = ();
335
336    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
337        State {
338            ids: Ids::new(id_gen),
339            selected_recipe: None,
340        }
341    }
342
343    fn style(&self) -> Self::Style {}
344
345    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
346        common_base::prof_span!("Crafting::update");
347        let widget::UpdateArgs { state, ui, .. } = args;
348
349        let mut events = Vec::new();
350
351        // Handle any initialization
352        // TODO: Replace with struct instead of making assorted booleans once there is
353        // more than 1 field.
354        if self.show.crafting_fields.initialize_repair {
355            state.update(|s| {
356                s.selected_recipe = Some(String::from("veloren.core.pseudo_recipe.repair"))
357            });
358        }
359        self.show.crafting_fields.initialize_repair = false;
360
361        // Tooltips
362        let item_tooltip = ItemTooltip::new(
363            {
364                // Edge images [t, b, r, l]
365                // Corner images [tr, tl, br, bl]
366                let edge = &self.rot_imgs.tt_side;
367                let corner = &self.rot_imgs.tt_corner;
368                ImageFrame::new(
369                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
370                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
371                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
372                    5.0,
373                )
374            },
375            self.client,
376            self.info,
377            self.imgs,
378            self.item_imgs,
379            self.pulse,
380            self.msm,
381            self.rbm,
382            Some(self.inventory),
383            self.localized_strings,
384            self.item_i18n,
385        )
386        .title_font_size(self.fonts.cyri.scale(20))
387        .parent(ui.window)
388        .desc_font_size(self.fonts.cyri.scale(12))
389        .font_id(self.fonts.cyri.conrod_id)
390        .desc_text_color(TEXT_COLOR);
391        // Tab tooltips
392        let tabs_tooltip = Tooltip::new({
393            // Edge images [t, b, r, l]
394            // Corner images [tr, tl, br, bl]
395            let edge = &self.rot_imgs.tt_side;
396            let corner = &self.rot_imgs.tt_corner;
397            ImageFrame::new(
398                [edge.cw180, edge.none, edge.cw270, edge.cw90],
399                [corner.none, corner.cw270, corner.cw90, corner.cw180],
400                Color::Rgba(0.08, 0.07, 0.04, 1.0),
401                5.0,
402            )
403        })
404        .title_font_size(self.fonts.cyri.scale(15))
405        .parent(ui.window)
406        .desc_font_size(self.fonts.cyri.scale(12))
407        .font_id(self.fonts.cyri.conrod_id)
408        .desc_text_color(TEXT_COLOR);
409
410        let crafting_pos = self.global_state.settings.hud_position.crafting;
411        let crafting_window_size = Vec2::new(470.0, 460.0);
412
413        // Window
414        Image::new(self.imgs.crafting_window)
415            .bottom_right_with_margins_on(ui.window, crafting_pos.y, crafting_pos.x)
416            .color(Some(UI_MAIN))
417            .w_h(crafting_window_size.x, crafting_window_size.y)
418            .set(state.ids.window, ui);
419
420        // Frame
421        Image::new(self.imgs.crafting_frame)
422            .middle_of(state.ids.window)
423            .color(Some(UI_HIGHLIGHT_0))
424            .wh_of(state.ids.window)
425            .set(state.ids.window_frame, ui);
426
427        // Crafting Icon
428        Image::new(self.imgs.crafting_icon_bordered)
429            .w_h(38.0, 38.0)
430            .top_left_with_margins_on(state.ids.window_frame, 4.0, 4.0)
431            .set(state.ids.icon, ui);
432
433        // Close Button
434        if Button::image(self.imgs.close_btn)
435            .w_h(24.0, 25.0)
436            .hover_image(self.imgs.close_btn_hover)
437            .press_image(self.imgs.close_btn_press)
438            .top_right_with_margins_on(state.ids.window, 0.0, 0.0)
439            .set(state.ids.close, ui)
440            .was_clicked()
441        {
442            events.push(Event::Close);
443        }
444
445        // Title
446        Text::new(&self.localized_strings.get_msg("hud-crafting"))
447            .mid_top_with_margin_on(state.ids.window_frame, 9.0)
448            .font_id(self.fonts.cyri.conrod_id)
449            .font_size(self.fonts.cyri.scale(20))
450            .color(TEXT_COLOR)
451            .set(state.ids.title_main, ui);
452
453        // Alignment
454        Rectangle::fill_with([184.0, 380.0], color::TRANSPARENT)
455            .top_left_with_margins_on(state.ids.window_frame, 72.0, 5.0)
456            .scroll_kids_vertically()
457            .set(state.ids.align_rec, ui);
458        Rectangle::fill_with([274.0, 340.0], color::TRANSPARENT)
459            .top_right_with_margins_on(state.ids.window, 72.0, 5.0)
460            .scroll_kids_vertically()
461            .set(state.ids.align_ing, ui);
462
463        // Category Tabs
464        if state.ids.category_bgs.len() < CraftingTab::iter().enumerate().len() {
465            state.update(|s| {
466                s.ids.category_bgs.resize(
467                    CraftingTab::iter().enumerate().len(),
468                    &mut ui.widget_id_generator(),
469                )
470            })
471        };
472        if state.ids.category_tabs.len() < CraftingTab::iter().enumerate().len() {
473            state.update(|s| {
474                s.ids.category_tabs.resize(
475                    CraftingTab::iter().enumerate().len(),
476                    &mut ui.widget_id_generator(),
477                )
478            })
479        };
480        if state.ids.category_imgs.len() < CraftingTab::iter().enumerate().len() {
481            state.update(|s| {
482                s.ids.category_imgs.resize(
483                    CraftingTab::iter().enumerate().len(),
484                    &mut ui.widget_id_generator(),
485                )
486            })
487        };
488        let sel_crafting_tab = &self.show.crafting_fields.crafting_tab;
489        for (i, crafting_tab) in CraftingTab::iter()
490            .filter(|tab| !tab.is_adhoc())
491            .enumerate()
492        {
493            let tab_img = crafting_tab.img_id(self.imgs);
494            // Button Background
495            let mut bg = Image::new(self.imgs.pixel)
496                .w_h(40.0, 30.0)
497                .color(Some(UI_MAIN));
498            if i == 0 {
499                bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0)
500            } else {
501                bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0)
502            };
503            bg.set(state.ids.category_bgs[i], ui);
504            // Category Button
505            if Button::image(if crafting_tab == *sel_crafting_tab {
506                self.imgs.wpn_icon_border_pressed
507            } else {
508                self.imgs.wpn_icon_border
509            })
510            .wh_of(state.ids.category_bgs[i])
511            .middle_of(state.ids.category_bgs[i])
512            .hover_image(if crafting_tab == *sel_crafting_tab {
513                self.imgs.wpn_icon_border_pressed
514            } else {
515                self.imgs.wpn_icon_border_mo
516            })
517            .press_image(if crafting_tab == *sel_crafting_tab {
518                self.imgs.wpn_icon_border_pressed
519            } else {
520                self.imgs.wpn_icon_border_press
521            })
522            .with_tooltip(
523                self.tooltip_manager,
524                &self.localized_strings.get_msg(crafting_tab.name_key()),
525                "",
526                &tabs_tooltip,
527                TEXT_COLOR,
528            )
529            .set(state.ids.category_tabs[i], ui)
530            .was_clicked()
531            {
532                events.push(Event::ChangeCraftingTab(crafting_tab))
533            };
534            // Tab images
535            Image::new(tab_img)
536                .middle_of(state.ids.category_tabs[i])
537                .w_h(20.0, 20.0)
538                .graphics_for(state.ids.category_tabs[i])
539                .set(state.ids.category_imgs[i], ui);
540        }
541
542        // TODO: Consider UX for filtering searches, maybe a checkbox or a dropdown if
543        // more filters gets added
544        let mut _lower_case_search = String::new();
545        let (search_filter, search_keys) = {
546            if let Some(key) = &self.show.crafting_fields.crafting_search_key {
547                _lower_case_search = key.as_str().to_lowercase();
548                _lower_case_search
549                    .split_once(':')
550                    .map(|(filter, key)| {
551                        (
552                            SearchFilter::parse_from_str(filter),
553                            key.split_whitespace().collect(),
554                        )
555                    })
556                    .unwrap_or((
557                        SearchFilter::None,
558                        _lower_case_search.split_whitespace().collect(),
559                    ))
560            } else {
561                (SearchFilter::None, vec![])
562            }
563        };
564
565        let make_pseudo_recipe = |craft_sprite| Recipe {
566            output: (
567                Arc::<ItemDef>::load_expect_cloned("common.items.weapons.empty.empty"),
568                0,
569            ),
570            inputs: Vec::new(),
571            craft_sprite: Some(craft_sprite),
572        };
573
574        let weapon_recipe = make_pseudo_recipe(SpriteKind::CraftingBench);
575        let metal_comp_recipe = make_pseudo_recipe(SpriteKind::Anvil);
576        let wood_comp_recipe = make_pseudo_recipe(SpriteKind::CraftingBench);
577        let repair_recipe = make_pseudo_recipe(SpriteKind::RepairBench);
578
579        // A BTreeMap is used over a HashMap as when a HashMap is used, the UI shuffles
580        // the positions of these every tick, so a BTreeMap is necessary to keep it
581        // ordered.
582        let pseudo_entries = {
583            let mut pseudo_entries = BTreeMap::new();
584            pseudo_entries.insert(
585                String::from("veloren.core.pseudo_recipe.modular_weapon"),
586                (
587                    &weapon_recipe,
588                    self.localized_strings
589                        .get_msg("pseudo-recipe-modular_weapon-modular_weapon")
590                        .to_string(),
591                    CraftingTab::Weapon,
592                ),
593            );
594            pseudo_entries.insert(
595                String::from("veloren.core.pseudo_recipe.modular_weapon_component.sword"),
596                (
597                    &metal_comp_recipe,
598                    self.localized_strings
599                        .get_msg("pseudo-recipe-modular_weapon-sword")
600                        .to_string(),
601                    CraftingTab::Weapon,
602                ),
603            );
604            pseudo_entries.insert(
605                String::from("veloren.core.pseudo_recipe.modular_weapon_component.axe"),
606                (
607                    &metal_comp_recipe,
608                    self.localized_strings
609                        .get_msg("pseudo-recipe-modular_weapon-axe")
610                        .to_string(),
611                    CraftingTab::Weapon,
612                ),
613            );
614            pseudo_entries.insert(
615                String::from("veloren.core.pseudo_recipe.modular_weapon_component.hammer"),
616                (
617                    &metal_comp_recipe,
618                    self.localized_strings
619                        .get_msg("pseudo-recipe-modular_weapon-hammer")
620                        .to_string(),
621                    CraftingTab::Weapon,
622                ),
623            );
624            pseudo_entries.insert(
625                String::from("veloren.core.pseudo_recipe.modular_weapon_component.bow"),
626                (
627                    &wood_comp_recipe,
628                    self.localized_strings
629                        .get_msg("pseudo-recipe-modular_weapon-bow")
630                        .to_string(),
631                    CraftingTab::Weapon,
632                ),
633            );
634            pseudo_entries.insert(
635                String::from("veloren.core.pseudo_recipe.modular_weapon_component.staff"),
636                (
637                    &wood_comp_recipe,
638                    self.localized_strings
639                        .get_msg("pseudo-recipe-modular_weapon-staff")
640                        .to_string(),
641                    CraftingTab::Weapon,
642                ),
643            );
644            pseudo_entries.insert(
645                String::from("veloren.core.pseudo_recipe.modular_weapon_component.sceptre"),
646                (
647                    &wood_comp_recipe,
648                    self.localized_strings
649                        .get_msg("pseudo-recipe-modular_weapon-sceptre")
650                        .to_string(),
651                    CraftingTab::Weapon,
652                ),
653            );
654            pseudo_entries.insert(
655                String::from("veloren.core.pseudo_recipe.repair"),
656                (
657                    &repair_recipe,
658                    self.localized_strings
659                        .get_msg("pseudo-recipe-repair")
660                        .to_string(),
661                    CraftingTab::All,
662                ),
663            );
664            pseudo_entries
665        };
666
667        // First available recipes, then ones with available materials,
668        // then unavailable ones, each sorted by quality and then alphabetically
669        // In the tuple, "name" is the recipe book key, and "recipe.output.0.name()"
670        // is the display name (as stored in the item descriptors)
671        let search = |item: &Arc<ItemDef>| {
672            let (name_key, _) = item.i18n(self.item_i18n);
673            let fallback_name = self
674                .localized_strings
675                .get_content_fallback(&name_key)
676                .to_lowercase();
677            let name = self.localized_strings.get_content(&name_key).to_lowercase();
678
679            search_keys.iter().all(|&substring| {
680                name.contains(substring)
681                    || fallback_name.contains(substring)
682                    || name_key.hacky_descriptor().contains(substring)
683            })
684        };
685        let known_recipes = self
686            .inventory
687            .available_recipes_iter(self.rbm)
688            .map(|r| r.0.as_str())
689            .collect::<HashSet<_>>();
690        let recipe_source = if self.settings.gameplay.show_all_recipes {
691            Either::Left(
692                self.rbm
693                    .iter()
694                    .map(|r| (r, known_recipes.contains(r.0.as_str()))),
695            )
696        } else {
697            Either::Right(
698                self.inventory
699                    .available_recipes_iter(self.rbm)
700                    .map(|r| (r, true)),
701            )
702        };
703        let mut ordered_recipes: Vec<_> = recipe_source
704            .filter(|((_, recipe), _)| match search_filter {
705                SearchFilter::None => {
706                    search(&recipe.output.0)
707                },
708                SearchFilter::Input => recipe.inputs().any(|(input, _, _)| {
709                    let search_tag = |name: &str| {
710                        search_keys
711                            .iter()
712                            .all(|&substring| name.contains(substring))
713                    };
714                    match input {
715                        RecipeInput::Item(def) => search(def),
716                        RecipeInput::Tag(tag) => search_tag(tag.name()),
717                        RecipeInput::TagSameItem(tag) => search_tag(tag.name()),
718                        RecipeInput::ListSameItem(defs) => {
719                            defs.iter().any(search)
720                        },
721                    }
722                }),
723                SearchFilter::Nonexistent => false,
724            })
725            .map(|((name, recipe), known)| {
726                let has_materials = self.client.available_recipes().get(name.as_str()).is_some();
727                let is_craftable =
728                    self.client
729                        .available_recipes()
730                        .get(name.as_str()).is_some_and(|cs| {
731                            cs.is_none_or(|cs| {
732                                Some(cs) == self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
733                            })
734                        });
735                (name, recipe, is_craftable, has_materials, known)
736            })
737            .chain(
738                pseudo_entries
739                    .iter()
740                    // Filter by selected tab
741                    .filter(|(_, (_, _, tab))| *sel_crafting_tab == CraftingTab::All || sel_crafting_tab == tab)
742                    // Filter by search filter
743                    .filter(|(_, (_, output_name, _))| {
744                        match search_filter {
745                            SearchFilter::None => {
746                                let output_name = output_name.to_lowercase();
747                                search_keys
748                                    .iter()
749                                    .all(|&substring| output_name.contains(substring))
750                            },
751                            // TODO: Get input filtering to work here, probably requires
752                            // checking component recipe book?
753                            SearchFilter::Input => false,
754                            SearchFilter::Nonexistent => false,
755                        }
756                    })
757                    .map(|(recipe_name, (recipe, _, _))| {
758                        (
759                            recipe_name,
760                            *recipe,
761                            self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
762                                == recipe.craft_sprite,
763                            true,
764                            true,
765                        )
766                    }),
767            )
768            .collect();
769
770        ordered_recipes.sort_by_key(|(_, recipe, is_craftable, has_materials, known)| {
771            (
772                !known,
773                !is_craftable,
774                !has_materials,
775                recipe.output.0.quality(),
776                {
777                    let (title, _) = recipe.output.0.i18n(self.item_i18n);
778                    self.localized_strings.get_content(&title)
779                },
780            )
781        });
782
783        // Recipe list
784        let recipe_list_length = if self.settings.gameplay.show_all_recipes {
785            self.rbm.iter().count()
786        } else {
787            self.inventory.recipe_book_len()
788        } + pseudo_entries.len();
789
790        if state.ids.recipe_list_btns.len() < recipe_list_length {
791            state.update(|state| {
792                state
793                    .ids
794                    .recipe_list_btns
795                    .resize(recipe_list_length, &mut ui.widget_id_generator())
796            });
797        }
798        if state.ids.recipe_list_labels.len() < recipe_list_length {
799            state.update(|state| {
800                state
801                    .ids
802                    .recipe_list_labels
803                    .resize(recipe_list_length, &mut ui.widget_id_generator())
804            });
805        }
806        if state.ids.recipe_list_quality_indicators.len() < recipe_list_length {
807            state.update(|state| {
808                state
809                    .ids
810                    .recipe_list_quality_indicators
811                    .resize(recipe_list_length, &mut ui.widget_id_generator())
812            });
813        }
814        if state.ids.recipe_list_materials_indicators.len() < recipe_list_length {
815            state.update(|state| {
816                state
817                    .ids
818                    .recipe_list_materials_indicators
819                    .resize(recipe_list_length, &mut ui.widget_id_generator())
820            });
821        }
822        for (i, (name, recipe, is_craftable, has_materials, knows_recipe)) in ordered_recipes
823            .into_iter()
824            .filter(|(_, recipe, _, _, _)| self.show.crafting_fields.crafting_tab.satisfies(recipe))
825            .enumerate()
826        {
827            let button = Button::image(if state.selected_recipe.as_ref() == Some(name) {
828                self.imgs.selection
829            } else {
830                self.imgs.nothing
831            })
832            .and(|button| {
833                if i == 0 {
834                    button.top_left_with_margins_on(state.ids.align_rec, 2.0, 7.0)
835                } else {
836                    button.down_from(state.ids.recipe_list_btns[i - 1], 5.0)
837                }
838            })
839            .w(171.0)
840            .hover_image(self.imgs.selection_hover)
841            .press_image(self.imgs.selection_press)
842            .image_color(color::rgba(1.0, 0.82, 0.27, 1.0));
843
844            let title;
845            let recipe_name =
846                if let Some((_recipe, pseudo_name, _filter_tab)) = pseudo_entries.get(name) {
847                    pseudo_name
848                } else {
849                    (title, _) = util::item_text(
850                        recipe.output.0.as_ref(),
851                        self.localized_strings,
852                        self.item_i18n,
853                    );
854                    &title
855                };
856
857            let text = Text::new(recipe_name)
858                .color(if is_craftable {
859                    TEXT_COLOR
860                } else {
861                    TEXT_GRAY_COLOR
862                })
863                .font_size(self.fonts.cyri.scale(12))
864                .font_id(self.fonts.cyri.conrod_id)
865                .w(163.0)
866                .mid_top_with_margin_on(state.ids.recipe_list_btns[i], 3.0)
867                .graphics_for(state.ids.recipe_list_btns[i])
868                .center_justify();
869
870            let text_height = match text.get_y_dimension(ui) {
871                Dimension::Absolute(y) => y,
872                _ => 0.0,
873            };
874            let button_height = (text_height + 7.0).max(20.0);
875
876            if button
877                .h(button_height)
878                .set(state.ids.recipe_list_btns[i], ui)
879                .was_clicked()
880            {
881                if state.selected_recipe.as_ref() == Some(name) {
882                    state.update(|s| s.selected_recipe = None);
883                } else {
884                    if self.show.crafting_fields.crafting_tab.is_adhoc() {
885                        // If current tab is an adhoc tab, and recipe is selected, change to general
886                        // tab
887                        events.push(Event::ChangeCraftingTab(CraftingTab::All));
888                    }
889                    state.update(|s| s.selected_recipe = Some(name.clone()));
890                }
891                events.push(Event::ClearRecipeInputs);
892            }
893            // set the text here so that the correct position of the button is retrieved
894            text.set(state.ids.recipe_list_labels[i], ui);
895
896            // Sidebar color
897            let color::Hsla(h, s, l, _) = get_quality_col(recipe.output.0.quality()).to_hsl();
898            let val_multiplier = if is_craftable { 0.7 } else { 0.5 };
899            // Apply conversion to hsv, multiply v by the desired amount, then revert to
900            // hsl. Conversion formulae: https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion
901            // Note that division by 0 is not possible since none of the colours are black
902            // or white
903            let quality_col = color::hsl(
904                h,
905                s * val_multiplier * f32::min(l, 1.0 - l)
906                    / f32::min(l * val_multiplier, 1.0 - l * val_multiplier),
907                l * val_multiplier,
908            );
909
910            Button::image(self.imgs.quality_indicator)
911                .image_color(quality_col)
912                .w_h(4.0, button_height)
913                .left_from(state.ids.recipe_list_btns[i], 1.0)
914                .graphics_for(state.ids.recipe_list_btns[i])
915                .set(state.ids.recipe_list_quality_indicators[i], ui);
916
917            // Sidebar crafting tool icon
918            if !knows_recipe {
919                let recipe_img = "Recipe";
920
921                Button::image(animate_by_pulse(
922                    &self
923                        .item_imgs
924                        .img_ids_or_not_found_img(ItemKey::Simple(recipe_img.to_string())),
925                    self.pulse,
926                ))
927                .image_color(color::LIGHT_RED)
928                .w_h(button_height - 8.0, button_height - 8.0)
929                .top_left_with_margins_on(state.ids.recipe_list_btns[i], 4.0, 4.0)
930                .graphics_for(state.ids.recipe_list_btns[i])
931                .set(state.ids.recipe_list_materials_indicators[i], ui);
932            } else if has_materials && !is_craftable {
933                let station_img = match recipe.craft_sprite {
934                    Some(SpriteKind::Anvil) => Some("Anvil"),
935                    Some(SpriteKind::Cauldron) => Some("Cauldron"),
936                    Some(SpriteKind::CookingPot) => Some("CookingPot"),
937                    Some(SpriteKind::CraftingBench) => Some("CraftingBench"),
938                    Some(SpriteKind::Forge) => Some("Forge"),
939                    Some(SpriteKind::Loom) => Some("Loom"),
940                    Some(SpriteKind::SpinningWheel) => Some("SpinningWheel"),
941                    Some(SpriteKind::TanningRack) => Some("TanningRack"),
942                    Some(SpriteKind::DismantlingBench) => Some("DismantlingBench"),
943                    Some(SpriteKind::RepairBench) => Some("RepairBench"),
944                    _ => None,
945                };
946
947                if let Some(station_img_str) = station_img {
948                    Button::image(animate_by_pulse(
949                        &self
950                            .item_imgs
951                            .img_ids_or_not_found_img(ItemKey::Simple(station_img_str.to_string())),
952                        self.pulse,
953                    ))
954                    .image_color(color::LIGHT_RED)
955                    .w_h(button_height - 8.0, button_height - 8.0)
956                    .top_left_with_margins_on(state.ids.recipe_list_btns[i], 4.0, 4.0)
957                    .graphics_for(state.ids.recipe_list_btns[i])
958                    .set(state.ids.recipe_list_materials_indicators[i], ui);
959                }
960            }
961        }
962
963        // Deselect recipe if current tab is an adhoc tab, elsewhere if recipe selected
964        // while in an adhoc tab, tab is changed to general
965        if self.show.crafting_fields.crafting_tab.is_adhoc() {
966            state.update(|s| s.selected_recipe = None);
967        }
968
969        // Selected Recipe
970        if let Some((recipe_name, recipe)) = match state.selected_recipe.as_deref() {
971            Some(selected_recipe) => {
972                if let Some((modular_recipe, _pseudo_name, _filter_tab)) =
973                    pseudo_entries.get(selected_recipe)
974                {
975                    Some((selected_recipe, *modular_recipe))
976                } else {
977                    self.inventory
978                        .get_recipe(selected_recipe, self.rbm)
979                        .map(|r| (selected_recipe, r))
980                }
981            },
982            None => None,
983        } {
984            let recipe_name = String::from(recipe_name);
985
986            let title;
987            let title = if let Some((_recipe, pseudo_name, _filter_tab)) =
988                pseudo_entries.get(&recipe_name)
989            {
990                pseudo_name
991            } else {
992                (title, _) = util::item_text(
993                    recipe.output.0.as_ref(),
994                    self.localized_strings,
995                    self.item_i18n,
996                );
997                &title
998            };
999
1000            // Title
1001            Text::new(title)
1002                .mid_top_with_margin_on(state.ids.align_ing, -22.0)
1003                .font_id(self.fonts.cyri.conrod_id)
1004                .font_size(self.fonts.cyri.scale(14))
1005                .color(TEXT_COLOR)
1006                .parent(state.ids.window)
1007                .set(state.ids.title_ing, ui);
1008
1009            #[derive(Clone, Copy, Debug)]
1010            enum RecipeKind {
1011                ModularWeapon,
1012                Component(ToolKind),
1013                Simple,
1014                Repair,
1015            }
1016
1017            let recipe_kind = match recipe_name.as_str() {
1018                "veloren.core.pseudo_recipe.modular_weapon" => RecipeKind::ModularWeapon,
1019                "veloren.core.pseudo_recipe.modular_weapon_component.sword" => {
1020                    RecipeKind::Component(ToolKind::Sword)
1021                },
1022                "veloren.core.pseudo_recipe.modular_weapon_component.axe" => {
1023                    RecipeKind::Component(ToolKind::Axe)
1024                },
1025                "veloren.core.pseudo_recipe.modular_weapon_component.hammer" => {
1026                    RecipeKind::Component(ToolKind::Hammer)
1027                },
1028                "veloren.core.pseudo_recipe.modular_weapon_component.bow" => {
1029                    RecipeKind::Component(ToolKind::Bow)
1030                },
1031                "veloren.core.pseudo_recipe.modular_weapon_component.staff" => {
1032                    RecipeKind::Component(ToolKind::Staff)
1033                },
1034                "veloren.core.pseudo_recipe.modular_weapon_component.sceptre" => {
1035                    RecipeKind::Component(ToolKind::Sceptre)
1036                },
1037                "veloren.core.pseudo_recipe.repair" => RecipeKind::Repair,
1038                _ => RecipeKind::Simple,
1039            };
1040
1041            let mut slot_maker = SlotMaker {
1042                empty_slot: self.imgs.inv_slot,
1043                hovered_slot: self.imgs.skillbar_index,
1044                filled_slot: self.imgs.inv_slot,
1045                selected_slot: self.imgs.inv_slot_sel,
1046                background_color: Some(UI_MAIN),
1047                content_size: ContentSize {
1048                    width_height_ratio: 1.0,
1049                    max_fraction: 0.75,
1050                },
1051                selected_content_scale: 1.067,
1052                amount_font: self.fonts.cyri.conrod_id,
1053                amount_margins: Vec2::new(-4.0, 0.0),
1054                amount_font_size: self.fonts.cyri.scale(12),
1055                amount_text_color: TEXT_COLOR,
1056                content_source: self.inventory,
1057                image_source: self.item_imgs,
1058                slot_manager: Some(self.slot_manager),
1059                last_input: &self.global_state.window.last_input(),
1060                pulse: self.pulse,
1061            };
1062
1063            // Output slot, tags, and modular input slots
1064            let (craft_slot_1, craft_slot_2, can_perform, recipe_known) = match recipe_kind {
1065                RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
1066                    if state.ids.craft_slots.len() < 2 {
1067                        state.update(|s| {
1068                            s.ids.craft_slots.resize(2, &mut ui.widget_id_generator());
1069                        });
1070                    }
1071
1072                    // Crafting instructions
1073                    Text::new(&self.localized_strings.get_msg("hud-crafting-modular_desc"))
1074                        .mid_top_of(state.ids.align_ing)
1075                        .w(264.0)
1076                        .center_justify()
1077                        .font_id(self.fonts.cyri.conrod_id)
1078                        .font_size(self.fonts.cyri.scale(13))
1079                        .color(TEXT_COLOR)
1080                        .set(state.ids.modular_desc_txt, ui);
1081
1082                    // Modular Weapon Crafting BG-Art
1083                    Image::new(self.imgs.crafting_modular_art)
1084                        .down_from(state.ids.modular_desc_txt, 15.0)
1085                        .align_middle_x()
1086                        .w_h(168.0, 250.0)
1087                        .set(state.ids.modular_art, ui);
1088
1089                    let primary_slot = CraftSlot {
1090                        index: 0,
1091                        slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
1092                        requirement: match recipe_kind {
1093                            RecipeKind::ModularWeapon => |item, _, _| {
1094                                matches!(
1095                                    &*item.kind(),
1096                                    ItemKind::ModularComponent(
1097                                        ModularComponent::ToolPrimaryComponent { .. }
1098                                    )
1099                                )
1100                            },
1101                            RecipeKind::Component(_) => |item, comp_recipes, info| {
1102                                if let Some(CraftSlotInfo::Tool(toolkind)) = info {
1103                                    comp_recipes
1104                                        .iter()
1105                                        .filter(|(key, _)| key.toolkind == toolkind)
1106                                        .any(|(key, _)| {
1107                                            Some(key.material.as_str())
1108                                                == item.item_definition_id().itemdef_id()
1109                                        })
1110                                } else {
1111                                    false
1112                                }
1113                            },
1114                            RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
1115                        },
1116                        info: match recipe_kind {
1117                            RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
1118                            RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
1119                                None
1120                            },
1121                        },
1122                    };
1123
1124                    let primary_slot_widget = slot_maker
1125                        .fabricate(primary_slot, [40.0; 2], false, false)
1126                        .top_left_with_margins_on(state.ids.modular_art, 4.0, 4.0)
1127                        .parent(state.ids.align_ing);
1128
1129                    if let Some(item) = primary_slot.item(self.inventory) {
1130                        primary_slot_widget
1131                            .with_item_tooltip(
1132                                self.item_tooltip_manager,
1133                                core::iter::once(item as &dyn ItemDesc),
1134                                &None,
1135                                &item_tooltip,
1136                            )
1137                            .set(state.ids.craft_slots[0], ui);
1138                    } else {
1139                        let (tooltip_title, tooltip_desc) = match recipe_kind {
1140                            RecipeKind::ModularWeapon => (
1141                                self.localized_strings
1142                                    .get_msg("hud-crafting-mod_weap_prim_slot_title"),
1143                                self.localized_strings
1144                                    .get_msg("hud-crafting-mod_weap_prim_slot_desc"),
1145                            ),
1146                            RecipeKind::Component(
1147                                ToolKind::Sword | ToolKind::Axe | ToolKind::Hammer,
1148                            ) => (
1149                                self.localized_strings
1150                                    .get_msg("hud-crafting-mod_comp_metal_prim_slot_title"),
1151                                self.localized_strings
1152                                    .get_msg("hud-crafting-mod_comp_metal_prim_slot_desc"),
1153                            ),
1154                            RecipeKind::Component(
1155                                ToolKind::Bow | ToolKind::Staff | ToolKind::Sceptre,
1156                            ) => (
1157                                self.localized_strings
1158                                    .get_msg("hud-crafting-mod_comp_wood_prim_slot_title"),
1159                                self.localized_strings
1160                                    .get_msg("hud-crafting-mod_comp_wood_prim_slot_desc"),
1161                            ),
1162                            RecipeKind::Component(_) | RecipeKind::Simple | RecipeKind::Repair => {
1163                                (Cow::Borrowed(""), Cow::Borrowed(""))
1164                            },
1165                        };
1166                        primary_slot_widget
1167                            .with_tooltip(
1168                                self.tooltip_manager,
1169                                &tooltip_title,
1170                                &tooltip_desc,
1171                                &tabs_tooltip,
1172                                TEXT_COLOR,
1173                            )
1174                            .set(state.ids.craft_slots[0], ui);
1175                    }
1176
1177                    let secondary_slot = CraftSlot {
1178                        index: 1,
1179                        slot: self.show.crafting_fields.recipe_inputs.get(&1).copied(),
1180                        requirement: match recipe_kind {
1181                            RecipeKind::ModularWeapon => |item, _, _| {
1182                                matches!(
1183                                    &*item.kind(),
1184                                    ItemKind::ModularComponent(
1185                                        ModularComponent::ToolSecondaryComponent { .. }
1186                                    )
1187                                )
1188                            },
1189                            RecipeKind::Component(_) => |item, comp_recipes, info| {
1190                                if let Some(CraftSlotInfo::Tool(toolkind)) = info {
1191                                    comp_recipes
1192                                        .iter()
1193                                        .filter(|(key, _)| key.toolkind == toolkind)
1194                                        .any(|(key, _)| {
1195                                            key.modifier.as_deref()
1196                                                == item.item_definition_id().itemdef_id()
1197                                        })
1198                                } else {
1199                                    false
1200                                }
1201                            },
1202                            RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
1203                        },
1204                        info: match recipe_kind {
1205                            RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
1206                            RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
1207                                None
1208                            },
1209                        },
1210                    };
1211
1212                    let secondary_slot_widget = slot_maker
1213                        .fabricate(secondary_slot, [40.0; 2], false, false)
1214                        .top_right_with_margins_on(state.ids.modular_art, 4.0, 4.0)
1215                        .parent(state.ids.align_ing);
1216
1217                    if let Some(item) = secondary_slot.item(self.inventory) {
1218                        secondary_slot_widget
1219                            .with_item_tooltip(
1220                                self.item_tooltip_manager,
1221                                core::iter::once(item as &dyn ItemDesc),
1222                                &None,
1223                                &item_tooltip,
1224                            )
1225                            .set(state.ids.craft_slots[1], ui);
1226                    } else {
1227                        let (tooltip_title, tooltip_desc) = match recipe_kind {
1228                            RecipeKind::ModularWeapon => (
1229                                self.localized_strings
1230                                    .get_msg("hud-crafting-mod_weap_sec_slot_title"),
1231                                self.localized_strings
1232                                    .get_msg("hud-crafting-mod_weap_sec_slot_desc"),
1233                            ),
1234                            RecipeKind::Component(_) => (
1235                                self.localized_strings
1236                                    .get_msg("hud-crafting-mod_comp_sec_slot_title"),
1237                                self.localized_strings
1238                                    .get_msg("hud-crafting-mod_comp_sec_slot_desc"),
1239                            ),
1240                            RecipeKind::Simple | RecipeKind::Repair => {
1241                                (Cow::Borrowed(""), Cow::Borrowed(""))
1242                            },
1243                        };
1244                        secondary_slot_widget
1245                            .with_tooltip(
1246                                self.tooltip_manager,
1247                                &tooltip_title,
1248                                &tooltip_desc,
1249                                &tabs_tooltip,
1250                                TEXT_COLOR,
1251                            )
1252                            .set(state.ids.craft_slots[1], ui);
1253                    }
1254
1255                    let prim_item_placed = primary_slot.slot.is_some();
1256                    let sec_item_placed = secondary_slot.slot.is_some();
1257
1258                    let prim_icon = match recipe_kind {
1259                        RecipeKind::ModularWeapon => self.imgs.icon_primary_comp,
1260                        RecipeKind::Component(ToolKind::Sword) => self.imgs.icon_ingot,
1261                        RecipeKind::Component(ToolKind::Axe) => self.imgs.icon_ingot,
1262                        RecipeKind::Component(ToolKind::Hammer) => self.imgs.icon_ingot,
1263                        RecipeKind::Component(ToolKind::Bow) => self.imgs.icon_log,
1264                        RecipeKind::Component(ToolKind::Staff) => self.imgs.icon_log,
1265                        RecipeKind::Component(ToolKind::Sceptre) => self.imgs.icon_log,
1266                        RecipeKind::Component(ToolKind::Shield) => self.imgs.icon_ingot,
1267                        _ => self.imgs.not_found,
1268                    };
1269
1270                    let sec_icon = match recipe_kind {
1271                        RecipeKind::ModularWeapon => self.imgs.icon_secondary_comp,
1272                        RecipeKind::Component(_) => self.imgs.icon_claw,
1273                        _ => self.imgs.not_found,
1274                    };
1275
1276                    // Output Image
1277                    Image::new(self.imgs.inv_slot)
1278                        .w_h(80.0, 80.0)
1279                        .mid_bottom_with_margin_on(state.ids.modular_art, 16.0)
1280                        .parent(state.ids.align_ing)
1281                        .set(state.ids.output_img_frame, ui);
1282                    let bg_col = Color::Rgba(1.0, 1.0, 1.0, 0.4);
1283                    if !prim_item_placed {
1284                        Image::new(prim_icon)
1285                            .middle_of(state.ids.craft_slots[0])
1286                            .color(Some(bg_col))
1287                            .w_h(34.0, 34.0)
1288                            .graphics_for(state.ids.craft_slots[0])
1289                            .set(state.ids.modular_wep_ing_1_bg, ui);
1290                    }
1291                    if !sec_item_placed {
1292                        Image::new(sec_icon)
1293                            .middle_of(state.ids.craft_slots[1])
1294                            .color(Some(bg_col))
1295                            .w_h(50.0, 50.0)
1296                            .graphics_for(state.ids.craft_slots[1])
1297                            .set(state.ids.modular_wep_ing_2_bg, ui);
1298                    }
1299
1300                    let ability_map = &AbilityMap::load().read();
1301                    let msm = &MaterialStatManifest::load().read();
1302
1303                    let (output_item, recipe_known) = match recipe_kind {
1304                        RecipeKind::ModularWeapon => {
1305                            let item = if let Some((primary_comp, toolkind, hand_restriction)) =
1306                                primary_slot.item(self.inventory).and_then(|item| {
1307                                    if let ItemKind::ModularComponent(
1308                                        ModularComponent::ToolPrimaryComponent {
1309                                            toolkind,
1310                                            hand_restriction,
1311                                            ..
1312                                        },
1313                                    ) = &*item.kind()
1314                                    {
1315                                        Some((item, *toolkind, *hand_restriction))
1316                                    } else {
1317                                        None
1318                                    }
1319                                }) {
1320                                secondary_slot
1321                                    .item(self.inventory)
1322                                    .filter(|item| {
1323                                        matches!(
1324                                            &*item.kind(),
1325                                            ItemKind::ModularComponent(
1326                                                ModularComponent::ToolSecondaryComponent { toolkind: toolkind_b, hand_restriction: hand_restriction_b, .. }
1327                                            ) if toolkind == *toolkind_b && modular::compatible_handedness(hand_restriction, *hand_restriction_b)
1328                                        )
1329                                    })
1330                                    .map(|secondary_comp| {
1331                                        Item::new_from_item_base(
1332                                            ItemBase::Modular(modular::ModularBase::Tool),
1333                                            vec![
1334                                                primary_comp.duplicate(ability_map, msm),
1335                                                secondary_comp.duplicate(ability_map, msm),
1336                                            ],
1337                                            ability_map,
1338                                            msm,
1339                                        )
1340                                    })
1341                            } else {
1342                                None
1343                            };
1344                            (item, true)
1345                        },
1346                        RecipeKind::Component(toolkind) => {
1347                            if let Some(material) =
1348                                primary_slot.item(self.inventory).and_then(|item| {
1349                                    item.item_definition_id().itemdef_id().map(String::from)
1350                                })
1351                            {
1352                                let component_key = ComponentKey {
1353                                    toolkind,
1354                                    material,
1355                                    modifier: secondary_slot.item(self.inventory).and_then(
1356                                        |item| {
1357                                            item.item_definition_id().itemdef_id().map(String::from)
1358                                        },
1359                                    ),
1360                                };
1361                                self.client
1362                                    .component_recipe_book()
1363                                    .get(&component_key)
1364                                    .map(|component_recipe| {
1365                                        let item = component_recipe.item_output(ability_map, msm);
1366                                        let learned = self
1367                                            .inventory
1368                                            .recipe_is_known(&component_recipe.recipe_book_key);
1369                                        (item, learned)
1370                                    })
1371                                    .map_or((None, true), |(item, known)| (Some(item), known))
1372                            } else {
1373                                (None, true)
1374                            }
1375                        },
1376                        RecipeKind::Simple | RecipeKind::Repair => (None, true),
1377                    };
1378
1379                    if let Some(output_item) = output_item {
1380                        let (name, _) =
1381                            util::item_text(&output_item, self.localized_strings, self.item_i18n);
1382                        Button::image(animate_by_pulse(
1383                            &self
1384                                .item_imgs
1385                                .img_ids_or_not_found_img(ItemKey::from(&output_item)),
1386                            self.pulse,
1387                        ))
1388                        .w_h(55.0, 55.0)
1389                        .label(&name)
1390                        .label_color(TEXT_COLOR)
1391                        .label_font_size(self.fonts.cyri.scale(14))
1392                        .label_font_id(self.fonts.cyri.conrod_id)
1393                        .label_y(conrod_core::position::Relative::Scalar(-64.0))
1394                        .label_x(conrod_core::position::Relative::Scalar(0.0))
1395                        .middle_of(state.ids.output_img_frame)
1396                        .with_item_tooltip(
1397                            self.item_tooltip_manager,
1398                            core::iter::once(&output_item as &dyn ItemDesc),
1399                            &None,
1400                            &item_tooltip,
1401                        )
1402                        .set(state.ids.output_img, ui);
1403                        (
1404                            primary_slot.slot,
1405                            secondary_slot.slot,
1406                            self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1407                                == recipe.craft_sprite,
1408                            recipe_known,
1409                        )
1410                    } else {
1411                        Image::new(self.imgs.icon_mod_weap)
1412                            .middle_of(state.ids.output_img_frame)
1413                            .color(Some(bg_col))
1414                            .w_h(70.0, 70.0)
1415                            .graphics_for(state.ids.output_img)
1416                            .set(state.ids.modular_wep_empty_bg, ui);
1417                        (primary_slot.slot, secondary_slot.slot, false, recipe_known)
1418                    }
1419                },
1420                RecipeKind::Simple => {
1421                    // Output Image Frame
1422                    let quality_col_img = match recipe.output.0.quality() {
1423                        Quality::Low => self.imgs.inv_slot_grey,
1424                        Quality::Common => self.imgs.inv_slot,
1425                        Quality::Moderate => self.imgs.inv_slot_green,
1426                        Quality::High => self.imgs.inv_slot_blue,
1427                        Quality::Epic => self.imgs.inv_slot_purple,
1428                        Quality::Legendary => self.imgs.inv_slot_gold,
1429                        Quality::Artifact => self.imgs.inv_slot_orange,
1430                        _ => self.imgs.inv_slot_red,
1431                    };
1432
1433                    Image::new(quality_col_img)
1434                        .w_h(60.0, 60.0)
1435                        .top_right_with_margins_on(state.ids.align_ing, 15.0, 10.0)
1436                        .parent(state.ids.align_ing)
1437                        .set(state.ids.output_img_frame, ui);
1438
1439                    let output_text = format!("x{}", recipe.output.1);
1440                    // Output Image
1441                    Button::image(animate_by_pulse(
1442                        &self
1443                            .item_imgs
1444                            .img_ids_or_not_found_img((&*recipe.output.0).into()),
1445                        self.pulse,
1446                    ))
1447                    .w_h(55.0, 55.0)
1448                    .label(&output_text)
1449                    .label_color(TEXT_COLOR)
1450                    .label_font_size(self.fonts.cyri.scale(14))
1451                    .label_font_id(self.fonts.cyri.conrod_id)
1452                    .label_y(conrod_core::position::Relative::Scalar(-24.0))
1453                    .label_x(conrod_core::position::Relative::Scalar(24.0))
1454                    .middle_of(state.ids.output_img_frame)
1455                    .with_item_tooltip(
1456                        self.item_tooltip_manager,
1457                        core::iter::once(&*recipe.output.0 as &dyn ItemDesc),
1458                        &None,
1459                        &item_tooltip,
1460                    )
1461                    .set(state.ids.output_img, ui);
1462
1463                    // Tags
1464                    if state.ids.tags_ing.len() < CraftingTab::iter().len() {
1465                        state.update(|state| {
1466                            state
1467                                .ids
1468                                .tags_ing
1469                                .resize(CraftingTab::iter().len(), &mut ui.widget_id_generator())
1470                        });
1471                    }
1472                    for (row, chunk) in CraftingTab::iter()
1473                        .filter(|crafting_tab| match crafting_tab {
1474                            CraftingTab::All => false,
1475                            _ => crafting_tab.satisfies(recipe),
1476                        })
1477                        .filter(|crafting_tab| {
1478                            crafting_tab != &self.show.crafting_fields.crafting_tab
1479                        })
1480                        .collect::<Vec<_>>()
1481                        .chunks(3)
1482                        .enumerate()
1483                    {
1484                        for (col, crafting_tab) in chunk.iter().rev().enumerate() {
1485                            let i = 3 * row + col;
1486                            let icon = Image::new(crafting_tab.img_id(self.imgs))
1487                                .w_h(20.0, 20.0)
1488                                .parent(state.ids.window);
1489                            let icon = if col == 0 {
1490                                icon.bottom_right_with_margins_on(
1491                                    state.ids.output_img_frame,
1492                                    -24.0 - 24.0 * (row as f64),
1493                                    4.0,
1494                                )
1495                            } else {
1496                                icon.left_from(state.ids.tags_ing[i - 1], 4.0)
1497                            };
1498                            icon.with_tooltip(
1499                                self.tooltip_manager,
1500                                &self.localized_strings.get_msg(crafting_tab.name_key()),
1501                                "",
1502                                &tabs_tooltip,
1503                                TEXT_COLOR,
1504                            )
1505                            .set(state.ids.tags_ing[i], ui);
1506                        }
1507                    }
1508                    (
1509                        None,
1510                        None,
1511                        self.client
1512                            .available_recipes()
1513                            .get(&recipe_name)
1514                            .is_some_and(|cs| {
1515                                cs.is_none_or(|cs| {
1516                                    Some(cs)
1517                                        == self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1518                                })
1519                            }),
1520                        true,
1521                    )
1522                },
1523                RecipeKind::Repair => {
1524                    if state.ids.craft_slots.is_empty() {
1525                        state.update(|s| {
1526                            s.ids.craft_slots.resize(1, &mut ui.widget_id_generator());
1527                        });
1528                    }
1529                    if state.ids.repair_buttons.len() < 2 {
1530                        state.update(|s| {
1531                            s.ids
1532                                .repair_buttons
1533                                .resize(2, &mut ui.widget_id_generator());
1534                        });
1535                    }
1536
1537                    // Repair instructions
1538                    Text::new(&self.localized_strings.get_msg("hud-crafting-repair_desc"))
1539                        .mid_top_of(state.ids.align_ing)
1540                        .w(264.0)
1541                        .center_justify()
1542                        .font_id(self.fonts.cyri.conrod_id)
1543                        .font_size(self.fonts.cyri.scale(13))
1544                        .color(TEXT_COLOR)
1545                        .set(state.ids.modular_desc_txt, ui);
1546
1547                    // Slot for item to be repaired
1548                    let repair_slot = CraftSlot {
1549                        index: 0,
1550                        slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
1551                        requirement: |item, _, _| item.durability_lost().is_some_and(|d| d > 0),
1552                        info: None,
1553                    };
1554
1555                    let repair_slot_widget = slot_maker
1556                        .fabricate(repair_slot, [80.0; 2], false, false)
1557                        .down_from(state.ids.modular_desc_txt, 15.0)
1558                        .align_middle_x()
1559                        .parent(state.ids.align_ing);
1560
1561                    if let Some(item) = repair_slot.item(self.inventory) {
1562                        repair_slot_widget
1563                            .with_item_tooltip(
1564                                self.item_tooltip_manager,
1565                                core::iter::once(item as &dyn ItemDesc),
1566                                &None,
1567                                &item_tooltip,
1568                            )
1569                            .set(state.ids.craft_slots[0], ui);
1570                    } else {
1571                        repair_slot_widget
1572                            .with_tooltip(
1573                                self.tooltip_manager,
1574                                &self
1575                                    .localized_strings
1576                                    .get_msg("hud-crafting-repair_slot_title"),
1577                                &self
1578                                    .localized_strings
1579                                    .get_msg("hud-crafting-repair_slot_desc"),
1580                                &tabs_tooltip,
1581                                TEXT_COLOR,
1582                            )
1583                            .set(state.ids.craft_slots[0], ui);
1584                    }
1585
1586                    if repair_slot.slot.is_none() {
1587                        Image::new(self.imgs.icon_mod_weap)
1588                            .middle_of(state.ids.craft_slots[0])
1589                            .w_h(70.0, 70.0)
1590                            .graphics_for(state.ids.craft_slots[0])
1591                            .set(state.ids.modular_wep_ing_1_bg, ui);
1592                    }
1593
1594                    let can_repair = |item: &Item| {
1595                        // Check that item needs to be repaired, and that inventory has sufficient
1596                        // materials to repair
1597                        item.durability_lost().is_some_and(|d| d > 0)
1598                    };
1599
1600                    let can_perform = self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1601                        == recipe.craft_sprite;
1602
1603                    let color = if can_perform {
1604                        TEXT_COLOR
1605                    } else {
1606                        TEXT_GRAY_COLOR
1607                    };
1608
1609                    let btn_image_hover = if can_perform {
1610                        self.imgs.button_hover
1611                    } else {
1612                        self.imgs.button
1613                    };
1614
1615                    let btn_image_press = if can_perform {
1616                        self.imgs.button_press
1617                    } else {
1618                        self.imgs.button
1619                    };
1620
1621                    // Repair equipped button
1622                    if Button::image(self.imgs.button)
1623                        .w_h(105.0, 25.0)
1624                        .hover_image(btn_image_hover)
1625                        .press_image(btn_image_press)
1626                        .label(
1627                            &self
1628                                .localized_strings
1629                                .get_msg("hud-crafting-repair_equipped"),
1630                        )
1631                        .label_y(conrod_core::position::Relative::Scalar(1.0))
1632                        .label_color(color)
1633                        .label_font_size(self.fonts.cyri.scale(12))
1634                        .label_font_id(self.fonts.cyri.conrod_id)
1635                        .image_color(color)
1636                        .down_from(state.ids.craft_slots[0], 45.0)
1637                        .x_relative_to(state.ids.craft_slots[0], 0.0)
1638                        .set(state.ids.repair_buttons[0], ui)
1639                        .was_clicked()
1640                        && can_perform
1641                    {
1642                        self.inventory
1643                            .equipped_items_with_slot()
1644                            .filter(|(_, item)| can_repair(item))
1645                            .for_each(|(slot, _)| {
1646                                events.push(Event::RepairItem {
1647                                    slot: Slot::Equip(slot),
1648                                });
1649                            })
1650                    }
1651
1652                    // Repair all button
1653                    if Button::image(self.imgs.button)
1654                        .w_h(105.0, 25.0)
1655                        .hover_image(btn_image_hover)
1656                        .press_image(btn_image_press)
1657                        .label(&self.localized_strings.get_msg("hud-crafting-repair_all"))
1658                        .label_y(conrod_core::position::Relative::Scalar(1.0))
1659                        .label_color(color)
1660                        .label_font_size(self.fonts.cyri.scale(12))
1661                        .label_font_id(self.fonts.cyri.conrod_id)
1662                        .image_color(color)
1663                        .down_from(state.ids.repair_buttons[0], 5.0)
1664                        .x_relative_to(state.ids.craft_slots[0], 0.0)
1665                        .set(state.ids.repair_buttons[1], ui)
1666                        .was_clicked()
1667                        && can_perform
1668                    {
1669                        self.inventory
1670                            .equipped_items_with_slot()
1671                            .filter(|(_, item)| can_repair(item))
1672                            .for_each(|(slot, _)| {
1673                                events.push(Event::RepairItem {
1674                                    slot: Slot::Equip(slot),
1675                                });
1676                            });
1677                        self.inventory
1678                            .slots_with_id()
1679                            .filter(|(_, item)| item.as_ref().is_some_and(can_repair))
1680                            .for_each(|(slot, _)| {
1681                                events.push(Event::RepairItem {
1682                                    slot: Slot::Inventory(slot),
1683                                });
1684                            });
1685                    }
1686
1687                    let can_perform =
1688                        repair_slot.item(self.inventory).is_some_and(can_repair) && can_perform;
1689
1690                    (repair_slot.slot, None, can_perform, true)
1691                },
1692            };
1693
1694            // Button Separator
1695            Line::centred([0.0, 0.0], [274.0, 0.0])
1696                .color(color::rgba(1.0, 1.0, 1.0, 0.1))
1697                .down_from(state.ids.align_ing, 0.0)
1698                .parent(state.ids.window)
1699                .set(state.ids.btn_separator, ui);
1700
1701            // Craft button
1702            let label = &match recipe_kind {
1703                RecipeKind::Repair => self
1704                    .localized_strings
1705                    .get_msg("hud-crafting-repair-selection"),
1706                _ => self.localized_strings.get_msg("hud-crafting-craft"),
1707            };
1708            let craft_button_init = Button::image(self.imgs.button)
1709                .w_h(105.0, 25.0)
1710                .hover_image(if can_perform {
1711                    self.imgs.button_hover
1712                } else {
1713                    self.imgs.button
1714                })
1715                .press_image(if can_perform {
1716                    self.imgs.button_press
1717                } else {
1718                    self.imgs.button
1719                })
1720                .label(label)
1721                .label_y(conrod_core::position::Relative::Scalar(1.0))
1722                .label_color(if can_perform {
1723                    TEXT_COLOR
1724                } else {
1725                    TEXT_GRAY_COLOR
1726                })
1727                .label_font_size(self.fonts.cyri.scale(12))
1728                .label_font_id(self.fonts.cyri.conrod_id)
1729                .image_color(if can_perform {
1730                    TEXT_COLOR
1731                } else {
1732                    TEXT_GRAY_COLOR
1733                })
1734                .and(|b| match recipe_kind {
1735                    RecipeKind::Repair => b
1736                        .down_from(state.ids.craft_slots[0], 15.0)
1737                        .x_relative_to(state.ids.craft_slots[0], 0.0)
1738                        .parent(state.ids.align_ing),
1739                    _ => b
1740                        .bottom_left_with_margins_on(state.ids.align_ing, -31.0, 10.0)
1741                        .parent(state.ids.window_frame),
1742                });
1743
1744            let craft_button = if !recipe_known {
1745                craft_button_init
1746                    .with_tooltip(
1747                        self.tooltip_manager,
1748                        &self
1749                            .localized_strings
1750                            .get_msg("hud-crafting-recipe-uncraftable"),
1751                        &self
1752                            .localized_strings
1753                            .get_msg("hud-crafting-recipe-unlearned"),
1754                        &tabs_tooltip,
1755                        TEXT_COLOR,
1756                    )
1757                    .set(state.ids.btn_craft, ui)
1758            } else {
1759                craft_button_init.set(state.ids.btn_craft, ui)
1760            };
1761
1762            if craft_button.was_clicked() && can_perform {
1763                match recipe_kind {
1764                    RecipeKind::ModularWeapon => {
1765                        if let (
1766                            Some(Slot::Inventory(primary_slot)),
1767                            Some(Slot::Inventory(secondary_slot)),
1768                        ) = (craft_slot_1, craft_slot_2)
1769                        {
1770                            events.push(Event::CraftModularWeapon {
1771                                primary_slot,
1772                                secondary_slot,
1773                            });
1774                        }
1775                    },
1776                    RecipeKind::Component(toolkind) => {
1777                        if let Some(Slot::Inventory(primary_slot)) = craft_slot_1 {
1778                            events.push(Event::CraftModularWeaponComponent {
1779                                toolkind,
1780                                material: primary_slot,
1781                                modifier: craft_slot_2.and_then(|slot| match slot {
1782                                    Slot::Inventory(slot) => Some(slot),
1783                                    Slot::Equip(_) => None,
1784                                    Slot::Overflow(_) => None,
1785                                }),
1786                            });
1787                        }
1788                    },
1789                    RecipeKind::Simple => events.push(Event::CraftRecipe {
1790                        recipe_name,
1791                        amount: 1,
1792                    }),
1793                    RecipeKind::Repair => {
1794                        if let Some(slot) = craft_slot_1 {
1795                            events.push(Event::RepairItem { slot });
1796                        }
1797                    },
1798                }
1799            }
1800
1801            // Craft All button
1802            if matches!(recipe_kind, RecipeKind::Simple)
1803                && Button::image(self.imgs.button)
1804                    .w_h(105.0, 25.0)
1805                    .hover_image(if can_perform {
1806                        self.imgs.button_hover
1807                    } else {
1808                        self.imgs.button
1809                    })
1810                    .press_image(if can_perform {
1811                        self.imgs.button_press
1812                    } else {
1813                        self.imgs.button
1814                    })
1815                    .label(&self.localized_strings.get_msg("hud-crafting-craft_all"))
1816                    .label_y(conrod_core::position::Relative::Scalar(1.0))
1817                    .label_color(if can_perform {
1818                        TEXT_COLOR
1819                    } else {
1820                        TEXT_GRAY_COLOR
1821                    })
1822                    .label_font_size(self.fonts.cyri.scale(12))
1823                    .label_font_id(self.fonts.cyri.conrod_id)
1824                    .image_color(if can_perform {
1825                        TEXT_COLOR
1826                    } else {
1827                        TEXT_GRAY_COLOR
1828                    })
1829                    .bottom_right_with_margins_on(state.ids.align_ing, -31.0, 10.0)
1830                    .parent(state.ids.window_frame)
1831                    .set(state.ids.btn_craft_all, ui)
1832                    .was_clicked()
1833                && can_perform
1834            {
1835                if let Some(selected_recipe) = &state.selected_recipe {
1836                    let amount = recipe.max_from_ingredients(self.inventory);
1837                    if amount > 0 {
1838                        events.push(Event::CraftRecipe {
1839                            recipe_name: selected_recipe.to_string(),
1840                            amount,
1841                        });
1842                    }
1843                } else {
1844                    error!("State shows no selected recipe when trying to craft multiple.");
1845                }
1846            };
1847
1848            // Crafting Station Info
1849            if recipe.craft_sprite.is_some() {
1850                Text::new(
1851                    &self
1852                        .localized_strings
1853                        .get_msg("hud-crafting-req_crafting_station"),
1854                )
1855                .font_id(self.fonts.cyri.conrod_id)
1856                .font_size(self.fonts.cyri.scale(18))
1857                .color(TEXT_COLOR)
1858                .and(|t| match recipe_kind {
1859                    RecipeKind::Simple => {
1860                        t.top_left_with_margins_on(state.ids.align_ing, 10.0, 5.0)
1861                    },
1862                    RecipeKind::ModularWeapon | RecipeKind::Component(_) => t
1863                        .down_from(state.ids.modular_art, 25.0)
1864                        .x_place_on(state.ids.align_ing, Place::Start(Some(5.0))),
1865                    RecipeKind::Repair => t
1866                        .down_from(state.ids.repair_buttons[1], 20.0)
1867                        .x_place_on(state.ids.align_ing, Place::Start(Some(5.0))),
1868                })
1869                .set(state.ids.req_station_title, ui);
1870                let station_img = match recipe.craft_sprite {
1871                    Some(SpriteKind::Anvil) => "Anvil",
1872                    Some(SpriteKind::Cauldron) => "Cauldron",
1873                    Some(SpriteKind::CookingPot) => "CookingPot",
1874                    Some(SpriteKind::CraftingBench) => "CraftingBench",
1875                    Some(SpriteKind::Forge) => "Forge",
1876                    Some(SpriteKind::Loom) => "Loom",
1877                    Some(SpriteKind::SpinningWheel) => "SpinningWheel",
1878                    Some(SpriteKind::TanningRack) => "TanningRack",
1879                    Some(SpriteKind::DismantlingBench) => "DismantlingBench",
1880                    Some(SpriteKind::RepairBench) => "RepairBench",
1881                    None => "CraftsmanHammer",
1882                    _ => "CraftsmanHammer",
1883                };
1884                Image::new(animate_by_pulse(
1885                    &self
1886                        .item_imgs
1887                        .img_ids_or_not_found_img(ItemKey::Simple(station_img.to_string())),
1888                    self.pulse,
1889                ))
1890                .w_h(25.0, 25.0)
1891                .down_from(state.ids.req_station_title, 10.0)
1892                .parent(state.ids.align_ing)
1893                .set(state.ids.req_station_img, ui);
1894
1895                let station_name = match recipe.craft_sprite {
1896                    Some(SpriteKind::Anvil) => "hud-crafting-anvil",
1897                    Some(SpriteKind::Cauldron) => "hud-crafting-cauldron",
1898                    Some(SpriteKind::CookingPot) => "hud-crafting-cooking_pot",
1899                    Some(SpriteKind::CraftingBench) => "hud-crafting-crafting_bench",
1900                    Some(SpriteKind::Forge) => "hud-crafting-forge",
1901                    Some(SpriteKind::Loom) => "hud-crafting-loom",
1902                    Some(SpriteKind::SpinningWheel) => "hud-crafting-spinning_wheel",
1903                    Some(SpriteKind::TanningRack) => "hud-crafting-tanning_rack",
1904                    Some(SpriteKind::DismantlingBench) => "hud-crafting-salvaging_station",
1905                    Some(SpriteKind::RepairBench) => "hud-crafting-repair_bench",
1906                    _ => "",
1907                };
1908                Text::new(&self.localized_strings.get_msg(station_name))
1909                    .right_from(state.ids.req_station_img, 10.0)
1910                    .font_id(self.fonts.cyri.conrod_id)
1911                    .font_size(self.fonts.cyri.scale(14))
1912                    .color(
1913                        if self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1914                            == recipe.craft_sprite
1915                        {
1916                            TEXT_COLOR
1917                        } else {
1918                            TEXT_DULL_RED_COLOR
1919                        },
1920                    )
1921                    .set(state.ids.req_station_txt, ui);
1922            }
1923            // Ingredients Text
1924            // Hack from Sharp to account for iterators not having the same type
1925            let (mut iter_a, mut iter_b, mut iter_c);
1926            let ingredients = match recipe_kind {
1927                RecipeKind::Simple => {
1928                    iter_a = recipe
1929                        .inputs
1930                        .iter()
1931                        .map(|(recipe, amount, _)| (recipe, *amount));
1932                    &mut iter_a as &mut dyn ExactSizeIterator<Item = (&RecipeInput, u32)>
1933                },
1934                RecipeKind::ModularWeapon | RecipeKind::Repair => {
1935                    iter_b = core::iter::empty();
1936                    &mut iter_b
1937                },
1938                RecipeKind::Component(toolkind) => {
1939                    if let Some(material) = craft_slot_1
1940                        .and_then(|slot| match slot {
1941                            Slot::Inventory(slot) => self.inventory.get(slot),
1942                            Slot::Equip(_) => None,
1943                            Slot::Overflow(_) => None,
1944                        })
1945                        .and_then(|item| item.item_definition_id().itemdef_id().map(String::from))
1946                    {
1947                        let component_key = ComponentKey {
1948                            toolkind,
1949                            material,
1950                            modifier: craft_slot_2
1951                                .and_then(|slot| match slot {
1952                                    Slot::Inventory(slot) => self.inventory.get(slot),
1953                                    Slot::Equip(_) => None,
1954                                    Slot::Overflow(_) => None,
1955                                })
1956                                .and_then(|item| {
1957                                    item.item_definition_id().itemdef_id().map(String::from)
1958                                }),
1959                        };
1960                        if let Some(comp_recipe) =
1961                            self.client.component_recipe_book().get(&component_key)
1962                        {
1963                            iter_c = comp_recipe.inputs();
1964                            &mut iter_c as &mut dyn ExactSizeIterator<Item = _>
1965                        } else {
1966                            iter_b = core::iter::empty();
1967                            &mut iter_b
1968                        }
1969                    } else {
1970                        iter_b = core::iter::empty();
1971                        &mut iter_b
1972                    }
1973                },
1974            };
1975
1976            let num_ingredients = ingredients.len();
1977            if num_ingredients > 0 {
1978                Text::new(&self.localized_strings.get_msg("hud-crafting-ingredients"))
1979                    .font_id(self.fonts.cyri.conrod_id)
1980                    .font_size(self.fonts.cyri.scale(18))
1981                    .color(TEXT_COLOR)
1982                    .and(|t| {
1983                        if recipe.craft_sprite.is_some() {
1984                            t.down_from(state.ids.req_station_img, 10.0)
1985                        } else {
1986                            t.top_left_with_margins_on(state.ids.align_ing, 10.0, 5.0)
1987                        }
1988                    })
1989                    .set(state.ids.ingredients_txt, ui);
1990
1991                // Ingredient images with tooltip
1992                if state.ids.ingredient_frame.len() < num_ingredients {
1993                    state.update(|state| {
1994                        state
1995                            .ids
1996                            .ingredient_frame
1997                            .resize(num_ingredients, &mut ui.widget_id_generator())
1998                    });
1999                };
2000                if state.ids.ingredients.len() < num_ingredients {
2001                    state.update(|state| {
2002                        state
2003                            .ids
2004                            .ingredients
2005                            .resize(num_ingredients, &mut ui.widget_id_generator())
2006                    });
2007                };
2008                if state.ids.ingredient_btn.len() < num_ingredients {
2009                    state.update(|state| {
2010                        state
2011                            .ids
2012                            .ingredient_btn
2013                            .resize(num_ingredients, &mut ui.widget_id_generator())
2014                    });
2015                };
2016                if state.ids.ingredient_img.len() < num_ingredients {
2017                    state.update(|state| {
2018                        state
2019                            .ids
2020                            .ingredient_img
2021                            .resize(num_ingredients, &mut ui.widget_id_generator())
2022                    });
2023                };
2024                if state.ids.req_text.len() < num_ingredients {
2025                    state.update(|state| {
2026                        state
2027                            .ids
2028                            .req_text
2029                            .resize(num_ingredients, &mut ui.widget_id_generator())
2030                    });
2031                };
2032
2033                // Widget generation for every ingredient
2034                for (i, (recipe_input, amount)) in ingredients.enumerate() {
2035                    let item_def = match recipe_input {
2036                        RecipeInput::Item(item_def) => Some(Arc::clone(item_def)),
2037                        RecipeInput::Tag(tag) | RecipeInput::TagSameItem(tag) => self
2038                            .inventory
2039                            .slots()
2040                            .find_map(|slot| {
2041                                slot.as_ref().and_then(|item| {
2042                                    if item.matches_recipe_input(recipe_input, amount) {
2043                                        item.item_definition_id()
2044                                            .itemdef_id()
2045                                            .map(Arc::<ItemDef>::load_expect_cloned)
2046                                    } else {
2047                                        None
2048                                    }
2049                                })
2050                            })
2051                            .or_else(|| {
2052                                tag.exemplar_identifier()
2053                                    .map(Arc::<ItemDef>::load_expect_cloned)
2054                            }),
2055                        RecipeInput::ListSameItem(item_defs) => self
2056                            .inventory
2057                            .slots()
2058                            .find_map(|slot| {
2059                                slot.as_ref().and_then(|item| {
2060                                    if item.matches_recipe_input(recipe_input, amount) {
2061                                        item.item_definition_id()
2062                                            .itemdef_id()
2063                                            .map(Arc::<ItemDef>::load_expect_cloned)
2064                                    } else {
2065                                        None
2066                                    }
2067                                })
2068                            })
2069                            .or_else(|| {
2070                                item_defs.first().and_then(|i| {
2071                                    i.item_definition_id()
2072                                        .itemdef_id()
2073                                        .map(Arc::<ItemDef>::load_expect_cloned)
2074                                })
2075                            }),
2076                    };
2077
2078                    let item_def = if let Some(item_def) = item_def {
2079                        item_def
2080                    } else {
2081                        warn!(
2082                            "Failed to create example item def for recipe input {:?}",
2083                            recipe_input
2084                        );
2085                        continue;
2086                    };
2087
2088                    // Grey color for images and text if their amount is too low to craft the
2089                    // item
2090                    let item_count_in_inventory = self.inventory.item_count(&item_def);
2091                    let col = if item_count_in_inventory >= u64::from(amount.max(1)) {
2092                        TEXT_COLOR
2093                    } else {
2094                        TEXT_DULL_RED_COLOR
2095                    };
2096                    // Slot BG
2097                    let frame_pos = if i == 0 {
2098                        state.ids.ingredients_txt
2099                    } else {
2100                        state.ids.ingredient_frame[i - 1]
2101                    };
2102
2103                    let quality_col_img = match &item_def.quality() {
2104                        Quality::Low => self.imgs.inv_slot_grey,
2105                        Quality::Common => self.imgs.inv_slot,
2106                        Quality::Moderate => self.imgs.inv_slot_green,
2107                        Quality::High => self.imgs.inv_slot_blue,
2108                        Quality::Epic => self.imgs.inv_slot_purple,
2109                        Quality::Legendary => self.imgs.inv_slot_gold,
2110                        Quality::Artifact => self.imgs.inv_slot_orange,
2111                        _ => self.imgs.inv_slot_red,
2112                    };
2113                    let frame = Image::new(quality_col_img).w_h(25.0, 25.0);
2114                    let frame = if amount == 0 {
2115                        frame.down_from(state.ids.req_text[i], 10.0)
2116                    } else {
2117                        frame.down_from(frame_pos, 10.0)
2118                    };
2119                    frame.set(state.ids.ingredient_frame[i], ui);
2120
2121                    // Item button for auto search
2122                    if Button::image(self.imgs.wpn_icon_border)
2123                        .w_h(22.0, 22.0)
2124                        .middle_of(state.ids.ingredient_frame[i])
2125                        .hover_image(self.imgs.wpn_icon_border_mo)
2126                        .with_item_tooltip(
2127                            self.item_tooltip_manager,
2128                            core::iter::once(&*item_def as &dyn ItemDesc),
2129                            &None,
2130                            &item_tooltip,
2131                        )
2132                        .set(state.ids.ingredient_btn[i], ui)
2133                        .was_clicked()
2134                    {
2135                        events.push(Event::ChangeCraftingTab(CraftingTab::All));
2136
2137                        #[expect(deprecated)]
2138                        // TODO: what on earth are we doing here?
2139                        //
2140                        // oh wait, that's search by ingredient, yeah..
2141                        //
2142                        // well, we need a better logic here, use proper i18n
2143                        // here
2144                        events.push(Event::SearchRecipe(Some(
2145                            item_def.legacy_name().to_string(),
2146                        )));
2147                    }
2148
2149                    // Item image
2150                    Image::new(animate_by_pulse(
2151                        &self.item_imgs.img_ids_or_not_found_img((&*item_def).into()),
2152                        self.pulse,
2153                    ))
2154                    .middle_of(state.ids.ingredient_btn[i])
2155                    .w_h(20.0, 20.0)
2156                    .graphics_for(state.ids.ingredient_btn[i])
2157                    .with_item_tooltip(
2158                        self.item_tooltip_manager,
2159                        core::iter::once(&*item_def as &dyn ItemDesc),
2160                        &None,
2161                        &item_tooltip,
2162                    )
2163                    .set(state.ids.ingredient_img[i], ui);
2164
2165                    // Ingredients text and amount
2166                    // Don't show inventory amounts above 999 to avoid the widget clipping
2167                    let over9k = "99+";
2168                    let in_inv: &str = &item_count_in_inventory.to_string();
2169                    // Show Ingredients
2170                    // Align "Required" Text below last ingredient
2171                    if amount == 0 {
2172                        // Catalysts/Tools
2173                        let ref_widget = if i == 0 {
2174                            state.ids.ingredients_txt
2175                        } else {
2176                            state.ids.ingredient_frame[i - 1]
2177                        };
2178                        Text::new(&self.localized_strings.get_msg("hud-crafting-tool_cata"))
2179                            .down_from(ref_widget, 10.0)
2180                            .font_id(self.fonts.cyri.conrod_id)
2181                            .font_size(self.fonts.cyri.scale(18))
2182                            .color(TEXT_COLOR)
2183                            .set(state.ids.req_text[i], ui);
2184
2185                        let (name, _) = util::item_text(
2186                            item_def.as_ref(),
2187                            self.localized_strings,
2188                            self.item_i18n,
2189                        );
2190                        Text::new(&name)
2191                            .right_from(state.ids.ingredient_frame[i], 10.0)
2192                            .font_id(self.fonts.cyri.conrod_id)
2193                            .font_size(self.fonts.cyri.scale(14))
2194                            .color(col)
2195                            .set(state.ids.ingredients[i], ui);
2196                    } else {
2197                        // Ingredients
2198                        let name = match recipe_input {
2199                            RecipeInput::Item(_) => {
2200                                let (name, _) = util::item_text(
2201                                    item_def.as_ref(),
2202                                    self.localized_strings,
2203                                    self.item_i18n,
2204                                );
2205
2206                                name
2207                            },
2208                            RecipeInput::Tag(tag) | RecipeInput::TagSameItem(tag) => {
2209                                // TODO: Localize!
2210                                format!("Any {} item", tag.name())
2211                            },
2212                            RecipeInput::ListSameItem(item_defs) => {
2213                                // TODO: Localize!
2214                                format!(
2215                                    "Any of {}",
2216                                    item_defs
2217                                        .iter()
2218                                        .map(|def| {
2219                                            let (name, _) = util::item_text(
2220                                                def.as_ref(),
2221                                                self.localized_strings,
2222                                                self.item_i18n,
2223                                            );
2224
2225                                            name
2226                                        })
2227                                        .collect::<String>()
2228                                )
2229                            },
2230                        };
2231                        let input = format!(
2232                            "{}x {} ({})",
2233                            amount,
2234                            name,
2235                            if item_count_in_inventory > 99 {
2236                                over9k
2237                            } else {
2238                                in_inv
2239                            }
2240                        );
2241                        // Ingredient Text
2242                        Text::new(&input)
2243                            .right_from(state.ids.ingredient_frame[i], 10.0)
2244                            .font_id(self.fonts.cyri.conrod_id)
2245                            .font_size(self.fonts.cyri.scale(12))
2246                            .color(col)
2247                            .wrap_by_word()
2248                            .w(150.0)
2249                            .set(state.ids.ingredients[i], ui);
2250                    }
2251                }
2252            }
2253        } else if *sel_crafting_tab == CraftingTab::Dismantle {
2254            // Title
2255            Text::new(
2256                &self
2257                    .localized_strings
2258                    .get_msg("hud-crafting-dismantle_title"),
2259            )
2260            .mid_top_with_margin_on(state.ids.align_ing, 0.0)
2261            .font_id(self.fonts.cyri.conrod_id)
2262            .font_size(self.fonts.cyri.scale(24))
2263            .color(TEXT_COLOR)
2264            .parent(state.ids.window)
2265            .set(state.ids.dismantle_title, ui);
2266
2267            // Bench Icon
2268            let size = 140.0;
2269            Image::new(animate_by_pulse(
2270                &self
2271                    .item_imgs
2272                    .img_ids_or_not_found_img(ItemKey::Simple("DismantlingBench".to_string())),
2273                self.pulse,
2274            ))
2275            .wh([size; 2])
2276            .mid_top_with_margin_on(state.ids.align_ing, 50.0)
2277            .parent(state.ids.align_ing)
2278            .set(state.ids.dismantle_img, ui);
2279
2280            // Explanation
2281
2282            Text::new(
2283                &self
2284                    .localized_strings
2285                    .get_msg("hud-crafting-dismantle_explanation"),
2286            )
2287            .mid_bottom_with_margin_on(state.ids.dismantle_img, -60.0)
2288            .font_id(self.fonts.cyri.conrod_id)
2289            .font_size(self.fonts.cyri.scale(14))
2290            .color(TEXT_COLOR)
2291            .parent(state.ids.window)
2292            .set(state.ids.dismantle_txt, ui);
2293        }
2294
2295        // Search / Title Recipes
2296        if let Some(key) = &self.show.crafting_fields.crafting_search_key {
2297            if Button::image(self.imgs.close_btn)
2298                .top_left_with_margins_on(state.ids.align_rec, -20.0, 5.0)
2299                .w_h(14.0, 14.0)
2300                .hover_image(self.imgs.close_btn_hover)
2301                .press_image(self.imgs.close_btn_press)
2302                .parent(state.ids.window)
2303                .set(state.ids.btn_close_search, ui)
2304                .was_clicked()
2305            {
2306                events.push(Event::SearchRecipe(None));
2307            }
2308            Rectangle::fill([162.0, 20.0])
2309                .top_left_with_margins_on(state.ids.btn_close_search, -2.0, 16.0)
2310                .hsla(0.0, 0.0, 0.0, 0.7)
2311                .depth(1.0)
2312                .parent(state.ids.window)
2313                .set(state.ids.input_bg_search, ui);
2314            if let Some(string) = TextEdit::new(key.as_str())
2315                .top_left_with_margins_on(state.ids.btn_close_search, -2.0, 18.0)
2316                .w_h(138.0, 20.0)
2317                .font_id(self.fonts.cyri.conrod_id)
2318                .font_size(self.fonts.cyri.scale(14))
2319                .color(TEXT_COLOR)
2320                .parent(state.ids.window)
2321                .set(state.ids.input_search, ui)
2322            {
2323                events.push(Event::SearchRecipe(Some(string)));
2324            }
2325        } else {
2326            Text::new(&self.localized_strings.get_msg("hud-crafting-recipes"))
2327                .mid_top_with_margin_on(state.ids.align_rec, -22.0)
2328                .font_id(self.fonts.cyri.conrod_id)
2329                .font_size(self.fonts.cyri.scale(14))
2330                .color(TEXT_COLOR)
2331                .parent(state.ids.window)
2332                .set(state.ids.title_rec, ui);
2333            Rectangle::fill_with([148.0, 20.0], color::TRANSPARENT)
2334                .top_left_with_margins_on(state.ids.window, 52.0, 26.0)
2335                .graphics_for(state.ids.btn_open_search)
2336                .set(state.ids.input_overlay_search, ui);
2337            let (eye, eye_hover, eye_press, tooltip_key) =
2338                if self.settings.gameplay.show_all_recipes {
2339                    (
2340                        self.imgs.eye_open_btn,
2341                        self.imgs.eye_open_btn_hover,
2342                        self.imgs.eye_open_btn_press,
2343                        "hud-crafting-hide_unknown_recipes",
2344                    )
2345                } else {
2346                    (
2347                        self.imgs.eye_closed_btn,
2348                        self.imgs.eye_closed_btn_hover,
2349                        self.imgs.eye_closed_btn_press,
2350                        "hud-crafting-show_unknown_recipes",
2351                    )
2352                };
2353
2354            if Button::image(eye)
2355                .top_left_with_margins_on(state.ids.align_rec, -21.0, 5.0)
2356                .w_h(16.0, 16.0)
2357                .hover_image(eye_hover)
2358                .press_image(eye_press)
2359                .parent(state.ids.window)
2360                .with_tooltip(
2361                    self.tooltip_manager,
2362                    &self.localized_strings.get_msg(tooltip_key),
2363                    "",
2364                    &tabs_tooltip,
2365                    TEXT_COLOR,
2366                )
2367                .set(state.ids.btn_show_all_recipes, ui)
2368                .was_clicked()
2369            {
2370                events.push(Event::ShowAllRecipes(
2371                    !self.settings.gameplay.show_all_recipes,
2372                ));
2373            }
2374            if Button::image(self.imgs.search_btn)
2375                .right_from(state.ids.btn_show_all_recipes, 5.0)
2376                .w_h(16.0, 16.0)
2377                .hover_image(self.imgs.search_btn_hover)
2378                .press_image(self.imgs.search_btn_press)
2379                .parent(state.ids.window)
2380                .set(state.ids.btn_open_search, ui)
2381                .was_clicked()
2382            {
2383                events.push(Event::SearchRecipe(Some(String::new())));
2384                events.push(Event::Focus(state.ids.input_search));
2385            }
2386        }
2387
2388        // Scrollbars
2389        Scrollbar::y_axis(state.ids.align_rec)
2390            .thickness(5.0)
2391            .rgba(0.66, 0.66, 0.66, 1.0)
2392            .set(state.ids.scrollbar_rec, ui);
2393        Scrollbar::y_axis(state.ids.align_ing)
2394            .thickness(5.0)
2395            .rgba(0.66, 0.66, 0.66, 1.0)
2396            .set(state.ids.scrollbar_ing, ui);
2397
2398        if self
2399            .global_state
2400            .settings
2401            .interface
2402            .toggle_draggable_windows
2403        {
2404            // Draggable area
2405            let draggable_dim = [crafting_window_size.x, 48.0];
2406
2407            Rectangle::fill_with(draggable_dim, color::TRANSPARENT)
2408                .top_left_with_margin_on(state.ids.window_frame, 0.0)
2409                .set(state.ids.draggable_area, ui);
2410
2411            let pos_delta: Vec2<f64> = ui
2412                .widget_input(state.ids.draggable_area)
2413                .drags()
2414                .left()
2415                .map(|drag| Vec2::<f64>::from(drag.delta_xy))
2416                .sum();
2417
2418            // The crafting uses bottom_right_with_margins_on which means
2419            // which means we have to use positive margins to move left
2420            // so we have to invert the x value from the delta.
2421            let pos_delta = pos_delta.with_x(-pos_delta.x);
2422
2423            let crafting_window_size_with_tabs =
2424                crafting_window_size.with_x(crafting_window_size.x + 40.0);
2425            let window_clamp = Vec2::new(ui.win_w, ui.win_h) - crafting_window_size_with_tabs;
2426
2427            let new_pos = (crafting_pos + pos_delta)
2428                .map(|e| e.max(0.))
2429                .map2(window_clamp, |e, bounds| e.min(bounds));
2430
2431            if new_pos.abs_diff_ne(&crafting_pos, f64::EPSILON) {
2432                events.push(Event::MoveCrafting(new_pos));
2433            }
2434
2435            if ui
2436                .widget_input(state.ids.draggable_area)
2437                .clicks()
2438                .right()
2439                .count()
2440                == 1
2441            {
2442                events.push(Event::MoveCrafting(HudPositionSettings::default().crafting));
2443            }
2444        }
2445
2446        events
2447    }
2448}