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