veloren_voxygen/ui/widgets/
item_tooltip.rs

1use super::image_frame::ImageFrame;
2use crate::hud::{
3    HudInfo, get_quality_col,
4    img_ids::Imgs,
5    item_imgs::{ItemImgs, animate_by_pulse},
6    util,
7};
8use client::Client;
9use common::{
10    comp::{
11        Energy, Inventory,
12        item::{
13            Item, ItemDesc, ItemI18n, ItemKind, ItemTag, MaterialStatManifest, Quality,
14            armor::Protection, item_key::ItemKey, modular::ModularComponent,
15        },
16    },
17    recipe::RecipeBookManifest,
18    trade::SitePrices,
19};
20use conrod_core::{
21    Color, Colorable, FontSize, Positionable, Scalar, Sizeable, Ui, UiCell, Widget, WidgetCommon,
22    WidgetStyle, builder_method, builder_methods, image, input::global::Global,
23    position::Dimension, text, widget, widget_ids,
24};
25use i18n::Localization;
26use lazy_static::lazy_static;
27use std::{
28    borrow::Borrow,
29    time::{Duration, Instant},
30};
31
32#[derive(Copy, Clone)]
33struct Hover(widget::Id, [f64; 2]);
34#[derive(Copy, Clone)]
35enum HoverState {
36    Hovering(Hover),
37    Fading(Instant, Hover, Option<(Instant, widget::Id)>),
38    Start(Instant, widget::Id),
39    None,
40}
41
42// Spacing between the tooltip and mouse
43const MOUSE_PAD_Y: f64 = 15.0;
44
45pub struct ItemTooltipManager {
46    state: HoverState,
47    // How long before a tooltip is displayed when hovering
48    hover_dur: Duration,
49    // How long it takes a tooltip to disappear
50    fade_dur: Duration,
51    // Current scaling of the ui
52    logical_scale_factor: f64,
53    // Ids for tooltip
54    tooltip_ids: widget::id::List,
55}
56
57impl ItemTooltipManager {
58    pub fn new(hover_dur: Duration, fade_dur: Duration, logical_scale_factor: f64) -> Self {
59        Self {
60            state: HoverState::None,
61            hover_dur,
62            fade_dur,
63            logical_scale_factor,
64            tooltip_ids: widget::id::List::new(),
65        }
66    }
67
68    pub fn maintain(&mut self, input: &Global, logical_scale_factor: f64) {
69        self.logical_scale_factor = logical_scale_factor;
70
71        let current = &input.current;
72
73        if let Some(um_id) = current.widget_under_mouse {
74            match self.state {
75                HoverState::Hovering(hover) if um_id == hover.0 => (),
76                HoverState::Hovering(hover) => {
77                    self.state =
78                        HoverState::Fading(Instant::now(), hover, Some((Instant::now(), um_id)))
79                },
80                HoverState::Fading(_, _, Some((_, id))) if um_id == id => {},
81                HoverState::Fading(start, hover, _) => {
82                    self.state = HoverState::Fading(start, hover, Some((Instant::now(), um_id)))
83                },
84                HoverState::Start(_, id) if um_id == id => (),
85                HoverState::Start(_, _) | HoverState::None => {
86                    self.state = HoverState::Start(Instant::now(), um_id)
87                },
88            }
89        } else {
90            match self.state {
91                HoverState::Hovering(hover) => {
92                    self.state = HoverState::Fading(Instant::now(), hover, None)
93                },
94                HoverState::Fading(start, hover, Some((_, _))) => {
95                    self.state = HoverState::Fading(start, hover, None)
96                },
97                HoverState::Start(_, _) => self.state = HoverState::None,
98                HoverState::Fading(_, _, None) | HoverState::None => (),
99            }
100        }
101
102        // Handle fade timing
103        if let HoverState::Fading(start, _, maybe_hover) = self.state {
104            if start.elapsed() > self.fade_dur {
105                self.state = match maybe_hover {
106                    Some((start, hover)) => HoverState::Start(start, hover),
107                    None => HoverState::None,
108                };
109            }
110        }
111    }
112
113    fn set_tooltip<'a, I>(
114        &mut self,
115        tooltip: &'a ItemTooltip,
116        items: impl Iterator<Item = I>,
117        prices: &'a Option<SitePrices>,
118        img_id: Option<image::Id>,
119        image_dims: Option<(f64, f64)>,
120        src_id: widget::Id,
121        ui: &mut UiCell,
122    ) where
123        I: Borrow<dyn ItemDesc>,
124    {
125        let mp_h = MOUSE_PAD_Y / self.logical_scale_factor;
126
127        let mut id_walker = self.tooltip_ids.walk();
128
129        let tooltip = |transparency, mouse_pos: [f64; 2], ui: &mut UiCell| {
130            let mut prev_id = None;
131            for item in items {
132                let tooltip_id =
133                    id_walker.next(&mut self.tooltip_ids, &mut ui.widget_id_generator());
134                // Fill in text and the potential image beforehand to get an accurate size for
135                // spacing
136                let tooltip = tooltip
137                    .clone()
138                    .item(item.borrow())
139                    .prices(prices)
140                    .image(img_id)
141                    .image_dims(image_dims);
142
143                let [t_w, t_h] = tooltip.get_wh(ui).unwrap_or([0.0, 0.0]);
144                let [m_x, m_y] = [mouse_pos[0], mouse_pos[1]];
145                let (w_w, w_h) = (ui.win_w, ui.win_h);
146
147                // Determine position based on size and mouse position
148                // Flow to the top left of the mouse when there is space
149                let x = if (m_x + w_w / 2.0) > t_w {
150                    m_x - t_w / 2.0
151                } else {
152                    m_x + t_w / 2.0
153                };
154                let y = if w_h - (m_y + w_h / 2.0) > t_h + mp_h {
155                    m_y + mp_h + t_h / 2.0
156                } else {
157                    m_y - mp_h - t_h / 2.0
158                };
159
160                if let Some(prev_id) = prev_id {
161                    tooltip
162                        .floating(true)
163                        .transparency(transparency)
164                        .up_from(prev_id, 5.0)
165                        .set(tooltip_id, ui);
166                } else {
167                    tooltip
168                        .floating(true)
169                        .transparency(transparency)
170                        .x_y(x, y)
171                        .set(tooltip_id, ui);
172                }
173
174                prev_id = Some(tooltip_id);
175            }
176        };
177        match self.state {
178            HoverState::Hovering(Hover(id, xy)) if id == src_id => tooltip(1.0, xy, ui),
179            HoverState::Fading(start, Hover(id, xy), _) if id == src_id => tooltip(
180                (0.1f32 - start.elapsed().as_millis() as f32 / self.hover_dur.as_millis() as f32)
181                    .max(0.0),
182                xy,
183                ui,
184            ),
185            HoverState::Start(start, id) if id == src_id && start.elapsed() > self.hover_dur => {
186                let xy = ui.global_input().current.mouse.xy;
187                self.state = HoverState::Hovering(Hover(id, xy));
188                tooltip(1.0, xy, ui);
189            },
190            _ => (),
191        }
192    }
193}
194
195pub struct ItemTooltipped<'a, W, I> {
196    inner: W,
197    tooltip_manager: &'a mut ItemTooltipManager,
198
199    items: I,
200    prices: &'a Option<SitePrices>,
201    img_id: Option<image::Id>,
202    image_dims: Option<(f64, f64)>,
203    tooltip: &'a ItemTooltip<'a>,
204}
205
206impl<W: Widget, I: Iterator> ItemTooltipped<'_, W, I> {
207    pub fn tooltip_image(mut self, img_id: image::Id) -> Self {
208        self.img_id = Some(img_id);
209        self
210    }
211
212    pub fn tooltip_image_dims(mut self, dims: (f64, f64)) -> Self {
213        self.image_dims = Some(dims);
214        self
215    }
216
217    pub fn set(self, id: widget::Id, ui: &mut UiCell) -> W::Event
218    where
219        <I as Iterator>::Item: Borrow<dyn ItemDesc>,
220    {
221        let event = self.inner.set(id, ui);
222        self.tooltip_manager.set_tooltip(
223            self.tooltip,
224            self.items,
225            self.prices,
226            self.img_id,
227            self.image_dims,
228            id,
229            ui,
230        );
231        event
232    }
233}
234
235pub trait ItemTooltipable {
236    // If `Tooltip` is expensive to construct accept a closure here instead.
237    fn with_item_tooltip<'a, I>(
238        self,
239        tooltip_manager: &'a mut ItemTooltipManager,
240
241        items: I,
242
243        prices: &'a Option<SitePrices>,
244
245        tooltip: &'a ItemTooltip<'a>,
246    ) -> ItemTooltipped<'a, Self, I>
247    where
248        Self: Sized;
249}
250impl<W: Widget> ItemTooltipable for W {
251    fn with_item_tooltip<'a, I>(
252        self,
253        tooltip_manager: &'a mut ItemTooltipManager,
254        items: I,
255        prices: &'a Option<SitePrices>,
256        tooltip: &'a ItemTooltip<'a>,
257    ) -> ItemTooltipped<'a, W, I> {
258        ItemTooltipped {
259            inner: self,
260            tooltip_manager,
261            items,
262            prices,
263            img_id: None,
264            image_dims: None,
265            tooltip,
266        }
267    }
268}
269
270/// Vertical spacing between elements of the tooltip
271const V_PAD: f64 = 10.0;
272/// Horizontal spacing between elements of the tooltip
273const H_PAD: f64 = 10.0;
274/// Vertical spacing between stats
275const V_PAD_STATS: f64 = 6.0;
276/// Default portion of inner width that goes to an image
277const IMAGE_W_FRAC: f64 = 0.3;
278/// Item icon size
279const ICON_SIZE: [f64; 2] = [64.0, 64.0];
280/// Total item tooltip width
281const WIDTH: f64 = 320.0;
282
283/// A widget for displaying tooltips
284#[derive(Clone, WidgetCommon)]
285pub struct ItemTooltip<'a> {
286    #[conrod(common_builder)]
287    common: widget::CommonBuilder,
288    item: &'a dyn ItemDesc,
289    msm: &'a MaterialStatManifest,
290    rbm: &'a RecipeBookManifest,
291    /// Inventory is optional because not all clients have an inventory
292    /// (spectator)
293    inventory: Option<&'a Inventory>,
294    prices: &'a Option<SitePrices>,
295    image: Option<image::Id>,
296    image_dims: Option<(f64, f64)>,
297    style: Style,
298    transparency: f32,
299    image_frame: ImageFrame,
300    client: &'a Client,
301    info: &'a HudInfo,
302    imgs: &'a Imgs,
303    item_imgs: &'a ItemImgs,
304    pulse: f32,
305    localized_strings: &'a Localization,
306    item_i18n: &'a ItemI18n,
307}
308
309#[derive(Clone, Debug, Default, PartialEq, WidgetStyle)]
310pub struct Style {
311    #[conrod(default = "Color::Rgba(1.0, 1.0, 1.0, 1.0)")]
312    pub color: Option<Color>,
313    title: widget::text::Style,
314    desc: widget::text::Style,
315    // add background imgs here
316}
317
318widget_ids! {
319    struct Ids {
320        title,
321        quantity,
322        subtitle,
323        desc,
324        prices_buy,
325        prices_sell,
326        tooltip_hints,
327        stats[],
328        diffs[],
329        item_frame,
330        item_render,
331        image_frame,
332        image,
333        background,
334    }
335}
336
337pub struct State {
338    ids: Ids,
339}
340
341lazy_static! {
342    static ref EMPTY_ITEM: Item = Item::new_from_asset_expect("common.items.weapons.empty.empty");
343}
344
345impl<'a> ItemTooltip<'a> {
346    builder_methods! {
347        pub desc_text_color { style.desc.color = Some(Color) }
348        pub title_font_size { style.title.font_size = Some(FontSize) }
349        pub desc_font_size { style.desc.font_size = Some(FontSize) }
350        pub title_justify { style.title.justify = Some(text::Justify) }
351        pub desc_justify { style.desc.justify = Some(text::Justify) }
352        pub title_line_spacing { style.title.line_spacing = Some(Scalar) }
353        pub desc_line_spacing { style.desc.line_spacing = Some(Scalar) }
354        image { image = Option<image::Id> }
355        item { item = &'a dyn ItemDesc }
356        prices { prices = &'a Option<SitePrices> }
357        msm { msm = &'a MaterialStatManifest }
358        rbm { rbm = &'a RecipeBookManifest }
359        image_dims { image_dims = Option<(f64, f64)> }
360        transparency { transparency = f32 }
361    }
362
363    pub fn new(
364        image_frame: ImageFrame,
365        client: &'a Client,
366        info: &'a HudInfo,
367        imgs: &'a Imgs,
368        item_imgs: &'a ItemImgs,
369        pulse: f32,
370        msm: &'a MaterialStatManifest,
371        rbm: &'a RecipeBookManifest,
372        inventory: Option<&'a Inventory>,
373        localized_strings: &'a Localization,
374        item_i18n: &'a ItemI18n,
375    ) -> Self {
376        ItemTooltip {
377            common: widget::CommonBuilder::default(),
378            style: Style::default(),
379            item: &*EMPTY_ITEM,
380            msm,
381            rbm,
382            inventory,
383            prices: &None,
384            transparency: 1.0,
385            image_frame,
386            image: None,
387            image_dims: None,
388            client,
389            info,
390            imgs,
391            item_imgs,
392            pulse,
393            localized_strings,
394            item_i18n,
395        }
396    }
397
398    // /// Align the text to the left of its bounding **Rect**'s *x* axis range.
399    // pub fn left_justify(self) -> Self {
400    //     self.justify(text::Justify::Left)
401    // }
402
403    // // Align the text to the middle of its bounding **Rect**'s *x* axis range.
404    // pub fn center_justify(self) -> Self {
405    //     self.justify(text::Justify::Center)
406    // }
407
408    // /// Align the text to the right of its bounding **Rect**'s *x* axis range.
409    // pub fn right_justify(self) -> Self {
410    //     self.justify(text::Justify::Right)
411    // }
412
413    fn text_image_width(&self, total_width: f64) -> (f64, f64) {
414        let inner_width = (total_width - H_PAD * 2.0).max(0.0);
415        // Image defaults to 30% of the width
416        let image_w = if self.image.is_some() {
417            match self.image_dims {
418                Some((w, _)) => w,
419                None => (inner_width - H_PAD).max(0.0) * IMAGE_W_FRAC,
420            }
421        } else {
422            0.0
423        };
424        // Text gets the remaining width
425        let text_w = (inner_width
426            - if self.image.is_some() {
427                image_w + H_PAD
428            } else {
429                0.0
430            })
431        .max(0.0);
432
433        (text_w, image_w)
434    }
435
436    /// Specify the font used for displaying the text.
437    #[must_use]
438    pub fn font_id(mut self, font_id: text::font::Id) -> Self {
439        self.style.title.font_id = Some(Some(font_id));
440        self.style.desc.font_id = Some(Some(font_id));
441        self
442    }
443}
444
445impl Widget for ItemTooltip<'_> {
446    type Event = ();
447    type State = State;
448    type Style = Style;
449
450    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
451        State {
452            ids: Ids::new(id_gen),
453        }
454    }
455
456    fn style(&self) -> Self::Style { self.style.clone() }
457
458    fn update(self, args: widget::UpdateArgs<Self>) {
459        let widget::UpdateArgs {
460            id,
461            state,
462            rect,
463            ui,
464            ..
465        } = args;
466
467        let i18n = &self.localized_strings;
468        let item_i18n = &self.item_i18n;
469
470        let inventories = self.client.inventories();
471        let inventory = match inventories.get(self.info.viewpoint_entity) {
472            Some(l) => l,
473            None => return,
474        };
475
476        let item = self.item;
477
478        let quality = get_quality_col(item.quality());
479
480        let item_kind = &*item.kind();
481
482        let equipped_item = inventory.equipped_items_replaceable_by(item_kind).next();
483
484        let (title, desc) = util::item_text(item, i18n, item_i18n);
485
486        let item_kind = util::kind_text(item_kind, i18n).to_string();
487
488        let material = item.tags().into_iter().find_map(|t| match t {
489            ItemTag::MaterialKind(material) => Some(material),
490            _ => None,
491        });
492
493        let subtitle = if let Some(material) = material {
494            format!(
495                "{} ({})",
496                item_kind,
497                util::material_kind_text(&material, i18n)
498            )
499        } else {
500            item_kind
501        };
502
503        let style = self.style.desc;
504
505        let text_color = conrod_core::color::WHITE;
506
507        // Widths
508        let (text_w, image_w) = self.text_image_width(rect.w());
509
510        // Color quality
511        let quality_col_img = match &item.quality() {
512            Quality::Low => self.imgs.inv_slot_grey,
513            Quality::Common => self.imgs.inv_slot_common,
514            Quality::Moderate => self.imgs.inv_slot_green,
515            Quality::High => self.imgs.inv_slot_blue,
516            Quality::Epic => self.imgs.inv_slot_purple,
517            Quality::Legendary => self.imgs.inv_slot_gold,
518            Quality::Artifact => self.imgs.inv_slot_orange,
519            _ => self.imgs.inv_slot_red,
520        };
521
522        let stats_count = util::stats_count(item, self.msm);
523
524        // Update widget array size
525        state.update(|s| {
526            s.ids
527                .stats
528                .resize(stats_count, &mut ui.widget_id_generator())
529        });
530
531        state.update(|s| {
532            s.ids
533                .diffs
534                .resize(stats_count, &mut ui.widget_id_generator())
535        });
536
537        // Background image frame
538        self.image_frame
539            .wh(rect.dim())
540            .xy(rect.xy())
541            .graphics_for(id)
542            .parent(id)
543            .color(quality)
544            .set(state.ids.image_frame, ui);
545
546        // Image
547        if let Some(img_id) = self.image {
548            widget::Image::new(img_id)
549                .w_h(image_w, self.image_dims.map_or(image_w, |(_, h)| h))
550                .graphics_for(id)
551                .parent(id)
552                .color(Some(quality))
553                .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD)
554                .set(state.ids.image, ui);
555        }
556
557        // Item frame
558        widget::Image::new(quality_col_img)
559            .wh(ICON_SIZE)
560            .graphics_for(id)
561            .parent(id)
562            .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD)
563            .set(state.ids.item_frame, ui);
564
565        // Item render
566        widget::Image::new(animate_by_pulse(
567            &self
568                .item_imgs
569                .img_ids_or_not_found_img(ItemKey::from(&item)),
570            self.pulse,
571        ))
572        .color(Some(conrod_core::color::WHITE))
573        .w_h(ICON_SIZE[0] * 0.8, ICON_SIZE[1] * 0.8)
574        .middle_of(state.ids.item_frame)
575        .set(state.ids.item_render, ui);
576
577        let title_w = (text_w - H_PAD * 3.0 - ICON_SIZE[0]).max(0.0);
578
579        // Title
580        widget::Text::new(&title)
581            .w(title_w)
582            .graphics_for(id)
583            .parent(id)
584            .with_style(self.style.title)
585            .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD)
586            .right_from(state.ids.item_frame, H_PAD)
587            .color(quality)
588            .set(state.ids.title, ui);
589
590        let amount = i18n.get_msg_ctx(
591            "items-common-amount",
592            &i18n::fluent_args! { "amount" => self.item.amount().get() },
593        );
594
595        // Amount
596        let (subtitle_relative_id, spacing) = if self.item.amount().get() > 1 {
597            widget::Text::new(&amount)
598                .w(title_w)
599                .graphics_for(id)
600                .parent(id)
601                .with_style(self.style.desc)
602                .color(conrod_core::color::GREY)
603                .down_from(state.ids.title, 2.0)
604                .set(state.ids.quantity, ui);
605
606            (state.ids.quantity, 2.0)
607        } else {
608            (state.ids.title, V_PAD)
609        };
610
611        // Subtitle
612        widget::Text::new(&subtitle)
613            .w(title_w)
614            .graphics_for(id)
615            .parent(id)
616            .with_style(self.style.desc)
617            .color(conrod_core::color::GREY)
618            .down_from(subtitle_relative_id, spacing)
619            .set(state.ids.subtitle, ui);
620
621        // Stats
622        match &*item.kind() {
623            ItemKind::Tool(tool) => {
624                let stats = tool.stats(item.stats_durability_multiplier());
625
626                // Power
627                widget::Text::new(&format!(
628                    "{} : {:.1}",
629                    i18n.get_msg("common-stats-power"),
630                    stats.power * 10.0
631                ))
632                .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
633                .graphics_for(id)
634                .parent(id)
635                .with_style(self.style.desc)
636                .color(text_color)
637                .down_from(state.ids.item_frame, V_PAD)
638                .set(state.ids.stats[0], ui);
639
640                let mut stat_text = |text: String, i: usize| {
641                    widget::Text::new(&text)
642                        .graphics_for(id)
643                        .parent(id)
644                        .with_style(self.style.desc)
645                        .color(text_color)
646                        .down_from(state.ids.stats[i - 1], V_PAD_STATS)
647                        .set(state.ids.stats[i], ui);
648                };
649
650                // Speed
651                stat_text(
652                    format!(
653                        "{} : {:+.0}%",
654                        i18n.get_msg("common-stats-speed"),
655                        (stats.speed - 1.0) * 100.0
656                    ),
657                    1,
658                );
659
660                // Effect Power
661                stat_text(
662                    format!(
663                        "{} : {:+.0}%",
664                        i18n.get_msg("common-stats-effect-power"),
665                        (stats.effect_power - 1.0) * 100.0
666                    ),
667                    2,
668                );
669
670                // Range
671                stat_text(
672                    format!(
673                        "{} : {:+.0}%",
674                        i18n.get_msg("common-stats-range"),
675                        (stats.range - 1.0) * 100.0
676                    ),
677                    3,
678                );
679
680                // Energy Efficiency
681                stat_text(
682                    format!(
683                        "{} : {:+.0}%",
684                        i18n.get_msg("common-stats-energy_efficiency"),
685                        (stats.energy_efficiency - 1.0) * 100.0
686                    ),
687                    4,
688                );
689
690                // Buff Strength
691                stat_text(
692                    format!(
693                        "{} : {:+.0}%",
694                        i18n.get_msg("common-stats-buff_strength"),
695                        (stats.buff_strength - 1.0) * 100.0
696                    ),
697                    5,
698                );
699
700                if item.has_durability() {
701                    let durability = Item::MAX_DURABILITY - item.durability_lost().unwrap_or(0);
702                    stat_text(
703                        format!(
704                            "{} : {}/{}",
705                            i18n.get_msg("common-stats-durability"),
706                            durability,
707                            Item::MAX_DURABILITY
708                        ),
709                        6,
710                    )
711                }
712
713                if let Some(equipped_item) = equipped_item {
714                    if let ItemKind::Tool(equipped_tool) = &*equipped_item.kind() {
715                        let tool_stats = tool.stats(item.stats_durability_multiplier());
716                        let equipped_tool_stats =
717                            equipped_tool.stats(equipped_item.stats_durability_multiplier());
718                        let diff = tool_stats - equipped_tool_stats;
719                        let power_diff =
720                            util::comparison(tool_stats.power, equipped_tool_stats.power);
721                        let speed_diff =
722                            util::comparison(tool_stats.speed, equipped_tool_stats.speed);
723                        let effect_power_diff = util::comparison(
724                            tool_stats.effect_power,
725                            equipped_tool_stats.effect_power,
726                        );
727                        let range_diff =
728                            util::comparison(tool_stats.range, equipped_tool_stats.range);
729                        let energy_efficiency_diff = util::comparison(
730                            tool_stats.energy_efficiency,
731                            equipped_tool_stats.energy_efficiency,
732                        );
733                        let buff_strength_diff = util::comparison(
734                            tool_stats.buff_strength,
735                            equipped_tool_stats.buff_strength,
736                        );
737
738                        let tool_durability =
739                            util::item_durability(item).unwrap_or(Item::MAX_DURABILITY);
740                        let equipped_durability =
741                            util::item_durability(equipped_item).unwrap_or(Item::MAX_DURABILITY);
742                        let durability_diff =
743                            util::comparison(tool_durability, equipped_durability);
744
745                        let mut diff_text = |text: String, color, id_index| {
746                            widget::Text::new(&text)
747                                .align_middle_y_of(state.ids.stats[id_index])
748                                .right_from(state.ids.stats[id_index], H_PAD)
749                                .graphics_for(id)
750                                .parent(id)
751                                .with_style(style)
752                                .color(color)
753                                .set(state.ids.diffs[id_index], ui)
754                        };
755
756                        if diff.power.abs() > f32::EPSILON {
757                            let text = format!("{} {:.1}", &power_diff.0, &diff.power * 10.0);
758                            diff_text(text, power_diff.1, 0)
759                        }
760                        if diff.speed.abs() > f32::EPSILON {
761                            let text = format!("{} {:+.0}", &speed_diff.0, &diff.speed * 100.0);
762                            diff_text(text, speed_diff.1, 1)
763                        }
764                        if diff.effect_power.abs() > f32::EPSILON {
765                            let text = format!(
766                                "{} {:+.0}",
767                                &effect_power_diff.0,
768                                &diff.effect_power * 100.0
769                            );
770                            diff_text(text, effect_power_diff.1, 2)
771                        }
772                        if diff.range.abs() > f32::EPSILON {
773                            let text = format!("{} {:.1}%", &range_diff.0, &diff.range * 100.0);
774                            diff_text(text, range_diff.1, 3)
775                        }
776                        if diff.energy_efficiency.abs() > f32::EPSILON {
777                            let text = format!(
778                                "{} {:.1}%",
779                                &energy_efficiency_diff.0,
780                                &diff.energy_efficiency * 100.0
781                            );
782                            diff_text(text, energy_efficiency_diff.1, 4)
783                        }
784                        if diff.buff_strength.abs() > f32::EPSILON {
785                            let text = format!(
786                                "{} {:.1}%",
787                                &buff_strength_diff.0,
788                                &diff.buff_strength * 100.0
789                            );
790                            diff_text(text, buff_strength_diff.1, 5)
791                        }
792                        if tool_durability != equipped_durability && item.has_durability() {
793                            let text = format!(
794                                "{} {}",
795                                &durability_diff.0,
796                                tool_durability as i32 - equipped_durability as i32
797                            );
798                            diff_text(text, durability_diff.1, 6)
799                        }
800                    }
801                }
802            },
803            ItemKind::Armor(armor) => {
804                let armor_stats = armor.stats(self.msm, item.stats_durability_multiplier());
805
806                let mut stat_text = |text: String, i: usize| {
807                    widget::Text::new(&text)
808                        .graphics_for(id)
809                        .parent(id)
810                        .with_style(self.style.desc)
811                        .color(text_color)
812                        .and(|t| {
813                            if i == 0 {
814                                t.x_align_to(
815                                    state.ids.item_frame,
816                                    conrod_core::position::Align::Start,
817                                )
818                                .down_from(state.ids.item_frame, V_PAD)
819                            } else {
820                                t.down_from(state.ids.stats[i - 1], V_PAD_STATS)
821                            }
822                        })
823                        .set(state.ids.stats[i], ui);
824                };
825
826                let mut index = 0;
827
828                if armor_stats.protection.is_some() {
829                    stat_text(
830                        format!(
831                            "{} : {}",
832                            i18n.get_msg("common-stats-armor"),
833                            util::protec2string(
834                                armor_stats.protection.unwrap_or(Protection::Normal(0.0))
835                            )
836                        ),
837                        index,
838                    );
839                    index += 1;
840                }
841
842                // Poise res
843                if armor_stats.poise_resilience.is_some() {
844                    stat_text(
845                        format!(
846                            "{} : {}",
847                            i18n.get_msg("common-stats-poise_res"),
848                            util::protec2string(
849                                armor_stats
850                                    .poise_resilience
851                                    .unwrap_or(Protection::Normal(0.0))
852                            )
853                        ),
854                        index,
855                    );
856                    index += 1;
857                }
858
859                // Max Energy
860                if armor_stats.energy_max.is_some() {
861                    stat_text(
862                        format!(
863                            "{} : {:.1}",
864                            i18n.get_msg("common-stats-energy_max"),
865                            armor_stats.energy_max.unwrap_or(0.0)
866                        ),
867                        index,
868                    );
869                    index += 1;
870                }
871
872                // Energy Recovery
873                if armor_stats.energy_reward.is_some() {
874                    stat_text(
875                        format!(
876                            "{} : {:.1}%",
877                            i18n.get_msg("common-stats-energy_reward"),
878                            armor_stats.energy_reward.map_or(0.0, |x| x * 100.0)
879                        ),
880                        index,
881                    );
882                    index += 1;
883                }
884
885                // Precision Power
886                if armor_stats.precision_power.is_some() {
887                    stat_text(
888                        format!(
889                            "{} : {:.3}",
890                            i18n.get_msg("common-stats-precision_power"),
891                            armor_stats.precision_power.unwrap_or(0.0)
892                        ),
893                        index,
894                    );
895                    index += 1;
896                }
897
898                // Stealth
899                if armor_stats.stealth.is_some() {
900                    stat_text(
901                        format!(
902                            "{} : {:.3}",
903                            i18n.get_msg("common-stats-stealth"),
904                            armor_stats.stealth.unwrap_or(0.0)
905                        ),
906                        index,
907                    );
908                    index += 1;
909                }
910
911                // Slots
912                if item.num_slots() > 0 {
913                    stat_text(
914                        format!(
915                            "{} : {}",
916                            i18n.get_msg("common-stats-slots"),
917                            item.num_slots()
918                        ),
919                        index,
920                    );
921                    index += 1;
922                }
923
924                if let Some(durability) = util::item_durability(item) {
925                    stat_text(
926                        format!(
927                            "{} : {}/{}",
928                            i18n.get_msg("common-stats-durability"),
929                            durability,
930                            Item::MAX_DURABILITY
931                        ),
932                        index,
933                    );
934                }
935
936                if let Some(equipped_item) = equipped_item {
937                    if let ItemKind::Armor(equipped_armor) = &*equipped_item.kind() {
938                        let equipped_stats = equipped_armor
939                            .stats(self.msm, equipped_item.stats_durability_multiplier());
940                        let diff = armor_stats - equipped_stats;
941                        let protection_diff = util::option_comparison(
942                            &armor_stats.protection,
943                            &equipped_stats.protection,
944                        );
945                        let poise_res_diff = util::option_comparison(
946                            &armor_stats.poise_resilience,
947                            &equipped_stats.poise_resilience,
948                        );
949                        let energy_max_diff = util::option_comparison(
950                            &armor_stats.energy_max,
951                            &equipped_stats.energy_max,
952                        );
953                        let energy_reward_diff = util::option_comparison(
954                            &armor_stats.energy_reward,
955                            &equipped_stats.energy_reward,
956                        );
957                        let precision_power_diff = util::option_comparison(
958                            &armor_stats.precision_power,
959                            &equipped_stats.precision_power,
960                        );
961                        let stealth_diff =
962                            util::option_comparison(&armor_stats.stealth, &equipped_stats.stealth);
963
964                        let armor_durability = util::item_durability(item);
965                        let equipped_durability = util::item_durability(equipped_item);
966                        let durability_diff =
967                            util::option_comparison(&armor_durability, &equipped_durability);
968
969                        let mut diff_text = |text: String, color, id_index| {
970                            widget::Text::new(&text)
971                                .align_middle_y_of(state.ids.stats[id_index])
972                                .right_from(state.ids.stats[id_index], H_PAD)
973                                .graphics_for(id)
974                                .parent(id)
975                                .with_style(style)
976                                .color(color)
977                                .set(state.ids.diffs[id_index], ui)
978                        };
979
980                        let mut index = 0;
981                        if let Some(p_diff) = diff.protection {
982                            if p_diff != Protection::Normal(0.0) {
983                                let text = format!(
984                                    "{} {}",
985                                    &protection_diff.0,
986                                    util::protec2string(p_diff)
987                                );
988                                diff_text(text, protection_diff.1, index);
989                            }
990                        }
991                        index += armor_stats.protection.is_some() as usize;
992
993                        if let Some(p_r_diff) = diff.poise_resilience {
994                            if p_r_diff != Protection::Normal(0.0) {
995                                let text = format!(
996                                    "{} {}",
997                                    &poise_res_diff.0,
998                                    util::protec2string(p_r_diff)
999                                );
1000                                diff_text(text, poise_res_diff.1, index);
1001                            }
1002                        }
1003                        index += armor_stats.poise_resilience.is_some() as usize;
1004
1005                        if let Some(e_m_diff) = diff.energy_max {
1006                            if e_m_diff.abs() > Energy::ENERGY_EPSILON {
1007                                let text = format!("{} {:.1}", &energy_max_diff.0, e_m_diff);
1008                                diff_text(text, energy_max_diff.1, index);
1009                            }
1010                        }
1011                        index += armor_stats.energy_max.is_some() as usize;
1012
1013                        if let Some(e_r_diff) = diff.energy_reward {
1014                            if e_r_diff.abs() > Energy::ENERGY_EPSILON {
1015                                let text =
1016                                    format!("{} {:.1}", &energy_reward_diff.0, e_r_diff * 100.0);
1017                                diff_text(text, energy_reward_diff.1, index);
1018                            }
1019                        }
1020                        index += armor_stats.energy_reward.is_some() as usize;
1021
1022                        if let Some(p_p_diff) = diff.precision_power {
1023                            if p_p_diff != 0.0_f32 {
1024                                let text = format!("{} {:.3}", &precision_power_diff.0, p_p_diff);
1025                                diff_text(text, precision_power_diff.1, index);
1026                            }
1027                        }
1028                        index += armor_stats.precision_power.is_some() as usize;
1029
1030                        if let Some(s_diff) = diff.stealth {
1031                            if s_diff != 0.0_f32 {
1032                                let text = format!("{} {:.3}", &stealth_diff.0, s_diff);
1033                                diff_text(text, stealth_diff.1, index);
1034                            }
1035                        }
1036                        index += armor_stats.stealth.is_some() as usize;
1037
1038                        if armor_durability != equipped_durability && item.has_durability() {
1039                            let diff = armor_durability.unwrap_or(Item::MAX_DURABILITY) as i32
1040                                - equipped_durability.unwrap_or(Item::MAX_DURABILITY) as i32;
1041                            let text = format!("{} {}", &durability_diff.0, diff);
1042                            diff_text(text, durability_diff.1, index);
1043                        }
1044                    }
1045                }
1046            },
1047            ItemKind::Consumable { effects, .. } => {
1048                for (i, desc) in util::consumable_desc(effects, i18n).iter().enumerate() {
1049                    let (down_from, pad) = match i {
1050                        0 => (state.ids.item_frame, V_PAD),
1051                        _ => (state.ids.stats[i - 1], V_PAD_STATS),
1052                    };
1053                    widget::Text::new(desc)
1054                        .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1055                        .graphics_for(id)
1056                        .parent(id)
1057                        .with_style(self.style.desc)
1058                        .color(text_color)
1059                        .down_from(down_from, pad)
1060                        .set(state.ids.stats[i], ui);
1061                }
1062            },
1063            ItemKind::RecipeGroup { recipes } => {
1064                const KNOWN_COLOR: Color = Color::Rgba(0.0, 1.0, 0.0, 1.0);
1065                const NOT_KNOWN_COLOR: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
1066                let recipe_known = self.inventory.map(|inv| {
1067                    inv.recipe_groups_iter()
1068                        .any(|group| group.item_definition_id() == item.item_definition_id())
1069                });
1070                let known_key_color = match recipe_known {
1071                    Some(false) => Some(("items-common-recipe-not_known", NOT_KNOWN_COLOR)),
1072                    Some(true) => Some(("items-common-recipe-known", KNOWN_COLOR)),
1073                    None => None,
1074                };
1075                if let Some((recipe_known_key, recipe_known_color)) = known_key_color {
1076                    let recipe_known_text = self.localized_strings.get_msg(recipe_known_key);
1077                    widget::Text::new(&recipe_known_text)
1078                        .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1079                        .graphics_for(id)
1080                        .parent(id)
1081                        .with_style(self.style.desc)
1082                        .color(recipe_known_color)
1083                        .down_from(state.ids.item_frame, V_PAD)
1084                        .set(state.ids.stats[0], ui);
1085                }
1086                let offset = if known_key_color.is_some() { 1 } else { 0 };
1087                for (i, desc) in recipes.iter().enumerate().map(|(i, s)| (i + offset, s)) {
1088                    if let Some(recipe) = self.rbm.get(desc) {
1089                        let (item_name, _) = util::item_text(
1090                            recipe.output.0.as_ref(),
1091                            self.localized_strings,
1092                            self.item_i18n,
1093                        );
1094                        let (down_from, pad) = match i {
1095                            0 => (state.ids.item_frame, V_PAD),
1096                            _ => (state.ids.stats[i - 1], V_PAD_STATS),
1097                        };
1098                        widget::Text::new(&item_name)
1099                            .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1100                            .graphics_for(id)
1101                            .parent(id)
1102                            .with_style(self.style.desc)
1103                            .color(text_color)
1104                            .down_from(down_from, pad)
1105                            .set(state.ids.stats[i], ui);
1106                    }
1107                }
1108            },
1109            ItemKind::ModularComponent(mc) => {
1110                if let Some(stats) = mc.tool_stats(item.components(), self.msm) {
1111                    let is_primary = matches!(mc, ModularComponent::ToolPrimaryComponent { .. });
1112
1113                    // Power
1114                    let power_text = if is_primary {
1115                        format!(
1116                            "{} : {:.1}",
1117                            i18n.get_msg("common-stats-power"),
1118                            stats.power * 10.0
1119                        )
1120                    } else {
1121                        format!(
1122                            "{} : x{:.2}",
1123                            i18n.get_msg("common-stats-power"),
1124                            stats.power
1125                        )
1126                    };
1127                    widget::Text::new(&power_text)
1128                        .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1129                        .graphics_for(id)
1130                        .parent(id)
1131                        .with_style(self.style.desc)
1132                        .color(text_color)
1133                        .down_from(state.ids.item_frame, V_PAD)
1134                        .set(state.ids.stats[0], ui);
1135
1136                    // Speed
1137                    let speed_text = if is_primary {
1138                        format!(
1139                            "{} : {:+.0}%",
1140                            i18n.get_msg("common-stats-speed"),
1141                            (stats.speed - 1.0) * 100.0
1142                        )
1143                    } else {
1144                        format!(
1145                            "{} : x{:.2}",
1146                            i18n.get_msg("common-stats-speed"),
1147                            stats.speed
1148                        )
1149                    };
1150                    widget::Text::new(&speed_text)
1151                        .graphics_for(id)
1152                        .parent(id)
1153                        .with_style(self.style.desc)
1154                        .color(text_color)
1155                        .down_from(state.ids.stats[0], V_PAD_STATS)
1156                        .set(state.ids.stats[1], ui);
1157
1158                    // Effect Power
1159                    // TODO: Allow effect power to have different terminology based on what it is
1160                    // affecting.
1161                    let effect_power_text = if is_primary {
1162                        format!(
1163                            "{} : {:+.0}%",
1164                            i18n.get_msg("common-stats-effect-power"),
1165                            (stats.effect_power - 1.0) * 100.0
1166                        )
1167                    } else {
1168                        format!(
1169                            "{} : x{:.2}",
1170                            i18n.get_msg("common-stats-effect-power"),
1171                            stats.effect_power
1172                        )
1173                    };
1174                    widget::Text::new(&effect_power_text)
1175                        .graphics_for(id)
1176                        .parent(id)
1177                        .with_style(self.style.desc)
1178                        .color(text_color)
1179                        .down_from(state.ids.stats[1], V_PAD_STATS)
1180                        .set(state.ids.stats[2], ui);
1181
1182                    // Range
1183                    let range_text = if is_primary {
1184                        format!(
1185                            "{} : {:.0}%",
1186                            i18n.get_msg("common-stats-range"),
1187                            (stats.range - 1.0) * 100.0
1188                        )
1189                    } else {
1190                        format!(
1191                            "{} : x{:.2}",
1192                            i18n.get_msg("common-stats-range"),
1193                            stats.range
1194                        )
1195                    };
1196                    widget::Text::new(&range_text)
1197                        .graphics_for(id)
1198                        .parent(id)
1199                        .with_style(self.style.desc)
1200                        .color(text_color)
1201                        .down_from(state.ids.stats[2], V_PAD_STATS)
1202                        .set(state.ids.stats[3], ui);
1203
1204                    // Energy Efficiency
1205                    let energy_eff_text = if is_primary {
1206                        format!(
1207                            "{} : {:.0}%",
1208                            i18n.get_msg("common-stats-energy_efficiency"),
1209                            (stats.energy_efficiency - 1.0) * 100.0
1210                        )
1211                    } else {
1212                        format!(
1213                            "{} : x{:.2}",
1214                            i18n.get_msg("common-stats-energy_efficiency"),
1215                            stats.energy_efficiency
1216                        )
1217                    };
1218                    widget::Text::new(&energy_eff_text)
1219                        .graphics_for(id)
1220                        .parent(id)
1221                        .with_style(self.style.desc)
1222                        .color(text_color)
1223                        .down_from(state.ids.stats[3], V_PAD_STATS)
1224                        .set(state.ids.stats[4], ui);
1225
1226                    // Buff Strength
1227                    let buff_str_text = if is_primary {
1228                        format!(
1229                            "{} : {:.0}%",
1230                            i18n.get_msg("common-stats-buff_strength"),
1231                            (stats.buff_strength - 1.0) * 100.0
1232                        )
1233                    } else {
1234                        format!(
1235                            "{} : x{:.2}",
1236                            i18n.get_msg("common-stats-buff_strength"),
1237                            stats.buff_strength
1238                        )
1239                    };
1240                    widget::Text::new(&buff_str_text)
1241                        .graphics_for(id)
1242                        .parent(id)
1243                        .with_style(self.style.desc)
1244                        .color(text_color)
1245                        .down_from(state.ids.stats[4], V_PAD_STATS)
1246                        .set(state.ids.stats[5], ui);
1247                }
1248            },
1249            _ => (),
1250        }
1251
1252        // Description
1253        if !desc.is_empty() {
1254            widget::Text::new(&format!("\"{}\"", &desc))
1255                .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1256                .graphics_for(id)
1257                .parent(id)
1258                .with_style(self.style.desc)
1259                .color(conrod_core::color::GREY)
1260                .down_from(
1261                    if stats_count > 0 {
1262                        state.ids.stats[state.ids.stats.len() - 1]
1263                    } else {
1264                        state.ids.item_frame
1265                    },
1266                    V_PAD,
1267                )
1268                .w(text_w)
1269                .set(state.ids.desc, ui);
1270        }
1271
1272        // Price display
1273        if let Some((buy, sell, factor)) =
1274            util::price_desc(self.prices, item.item_definition_id(), i18n)
1275        {
1276            widget::Text::new(&buy)
1277                .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1278                .graphics_for(id)
1279                .parent(id)
1280                .with_style(self.style.desc)
1281                .color(Color::Rgba(factor, 1.0 - factor, 0.00, 1.0))
1282                .down_from(
1283                    if !desc.is_empty() {
1284                        state.ids.desc
1285                    } else if stats_count > 0 {
1286                        state.ids.stats[state.ids.stats.len() - 1]
1287                    } else {
1288                        state.ids.item_frame
1289                    },
1290                    V_PAD,
1291                )
1292                .w(text_w)
1293                .set(state.ids.prices_buy, ui);
1294
1295            widget::Text::new(&sell)
1296                .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1297                .graphics_for(id)
1298                .parent(id)
1299                .with_style(self.style.desc)
1300                .color(Color::Rgba(1.0 - factor, factor, 0.00, 1.0))
1301                .down_from(state.ids.prices_buy, V_PAD_STATS)
1302                .w(text_w)
1303                .set(state.ids.prices_sell, ui);
1304
1305            //Tooltips for trade mini-tutorial
1306            widget::Text::new(&format!(
1307                "{}\n{}",
1308                i18n.get_msg("hud-trade-tooltip_hint_1"),
1309                i18n.get_msg("hud-trade-tooltip_hint_2"),
1310            ))
1311            .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1312            .graphics_for(id)
1313            .parent(id)
1314            .with_style(self.style.desc)
1315            .color(Color::Rgba(255.0, 255.0, 255.0, 1.0))
1316            .down_from(state.ids.prices_sell, V_PAD_STATS)
1317            .w(text_w)
1318            .set(state.ids.tooltip_hints, ui);
1319        }
1320    }
1321
1322    /// Default width is based on the description font size unless the text is
1323    /// small enough to fit on a single line
1324    fn default_x_dimension(&self, _ui: &Ui) -> Dimension { Dimension::Absolute(WIDTH) }
1325
1326    fn default_y_dimension(&self, ui: &Ui) -> Dimension {
1327        let item = &self.item;
1328
1329        let (_, desc) = util::item_text(item, self.localized_strings, self.item_i18n);
1330
1331        let (text_w, _image_w) = self.text_image_width(WIDTH);
1332
1333        // Item frame
1334        let frame_h = ICON_SIZE[1] + V_PAD;
1335
1336        // Stats
1337        let stats_count = util::line_count(self.item, self.msm, self.localized_strings);
1338        let stat_h = if stats_count > 0 {
1339            widget::Text::new("placeholder")
1340                .with_style(self.style.desc)
1341                .get_h(ui)
1342                .unwrap_or(0.0)
1343                * stats_count as f64
1344                + (stats_count - 1) as f64 * V_PAD_STATS
1345                + V_PAD
1346        } else {
1347            0.0
1348        };
1349
1350        // Description
1351        let desc_h: f64 = if !desc.is_empty() {
1352            widget::Text::new(&format!("\"{}\"", &desc))
1353                .with_style(self.style.desc)
1354                .w(text_w)
1355                .get_h(ui)
1356                .unwrap_or(0.0)
1357                + V_PAD
1358        } else {
1359            0.0
1360        };
1361
1362        // Price
1363        let price_h: f64 = if let Some((buy, sell, _)) = util::price_desc(
1364            self.prices,
1365            item.item_definition_id(),
1366            self.localized_strings,
1367        ) {
1368            // Get localized tooltip strings (gotten here because these should
1369            // only show if in a trade- aka if buy/sell prices are present)
1370            let tt_hint_1 = self.localized_strings.get_msg("hud-trade-tooltip_hint_1");
1371            let tt_hint_2 = self.localized_strings.get_msg("hud-trade-tooltip_hint_2");
1372
1373            widget::Text::new(&format!("{}\n{}\n{}\n{}", buy, sell, tt_hint_1, tt_hint_2))
1374                .with_style(self.style.desc)
1375                .w(text_w)
1376                .get_h(ui)
1377                .unwrap_or(0.0)
1378                + V_PAD * 2.0
1379        } else {
1380            0.0
1381        };
1382
1383        // extra padding to fit frame top padding
1384        let height = frame_h + stat_h + desc_h + price_h + V_PAD + 5.0;
1385        Dimension::Absolute(height)
1386    }
1387}
1388
1389impl Colorable for ItemTooltip<'_> {
1390    builder_method!(color { style.color = Some(Color) });
1391}