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