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