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