Skip to main content

veloren_voxygen/hud/
crafting.rs

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