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            && start.elapsed() > self.fade_dur
106        {
107            self.state = match maybe_hover {
108                Some((start, hover)) => HoverState::Start(start, hover),
109                None => HoverState::None,
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                    && let ItemKind::Tool(equipped_tool) = &*equipped_item.kind()
716                {
717                    let tool_stats = tool.stats(item.stats_durability_multiplier());
718                    let equipped_tool_stats =
719                        equipped_tool.stats(equipped_item.stats_durability_multiplier());
720                    let diff = tool_stats - equipped_tool_stats;
721                    let power_diff = util::comparison(tool_stats.power, equipped_tool_stats.power);
722                    let speed_diff = util::comparison(tool_stats.speed, equipped_tool_stats.speed);
723                    let effect_power_diff =
724                        util::comparison(tool_stats.effect_power, equipped_tool_stats.effect_power);
725                    let range_diff = util::comparison(tool_stats.range, equipped_tool_stats.range);
726                    let energy_efficiency_diff = util::comparison(
727                        tool_stats.energy_efficiency,
728                        equipped_tool_stats.energy_efficiency,
729                    );
730                    let buff_strength_diff = util::comparison(
731                        tool_stats.buff_strength,
732                        equipped_tool_stats.buff_strength,
733                    );
734
735                    let tool_durability =
736                        util::item_durability(item).unwrap_or(Item::MAX_DURABILITY);
737                    let equipped_durability =
738                        util::item_durability(equipped_item).unwrap_or(Item::MAX_DURABILITY);
739                    let durability_diff = util::comparison(tool_durability, equipped_durability);
740
741                    let mut diff_text = |text: String, color, id_index| {
742                        widget::Text::new(&text)
743                            .align_middle_y_of(state.ids.stats[id_index])
744                            .right_from(state.ids.stats[id_index], H_PAD)
745                            .graphics_for(id)
746                            .parent(id)
747                            .with_style(style)
748                            .color(color)
749                            .set(state.ids.diffs[id_index], ui)
750                    };
751
752                    if diff.power.abs() > f32::EPSILON {
753                        let text = format!("{} {:.1}", &power_diff.0, &diff.power * 10.0);
754                        diff_text(text, power_diff.1, 0)
755                    }
756                    if diff.speed.abs() > f32::EPSILON {
757                        let text = format!("{} {:+.0}", &speed_diff.0, &diff.speed * 100.0);
758                        diff_text(text, speed_diff.1, 1)
759                    }
760                    if diff.effect_power.abs() > f32::EPSILON {
761                        let text = format!(
762                            "{} {:+.0}",
763                            &effect_power_diff.0,
764                            &diff.effect_power * 100.0
765                        );
766                        diff_text(text, effect_power_diff.1, 2)
767                    }
768                    if diff.range.abs() > f32::EPSILON {
769                        let text = format!("{} {:.1}%", &range_diff.0, &diff.range * 100.0);
770                        diff_text(text, range_diff.1, 3)
771                    }
772                    if diff.energy_efficiency.abs() > f32::EPSILON {
773                        let text = format!(
774                            "{} {:.1}%",
775                            &energy_efficiency_diff.0,
776                            &diff.energy_efficiency * 100.0
777                        );
778                        diff_text(text, energy_efficiency_diff.1, 4)
779                    }
780                    if diff.buff_strength.abs() > f32::EPSILON {
781                        let text = format!(
782                            "{} {:.1}%",
783                            &buff_strength_diff.0,
784                            &diff.buff_strength * 100.0
785                        );
786                        diff_text(text, buff_strength_diff.1, 5)
787                    }
788                    if tool_durability != equipped_durability && item.has_durability() {
789                        let text = format!(
790                            "{} {}",
791                            &durability_diff.0,
792                            tool_durability as i32 - equipped_durability as i32
793                        );
794                        diff_text(text, durability_diff.1, 6)
795                    }
796                }
797            },
798            ItemKind::Armor(armor) => {
799                let armor_stats = armor.stats(self.msm, item.stats_durability_multiplier());
800
801                let mut stat_text = |text: String, i: usize| {
802                    widget::Text::new(&text)
803                        .graphics_for(id)
804                        .parent(id)
805                        .with_style(self.style.desc)
806                        .color(text_color)
807                        .and(|t| {
808                            if i == 0 {
809                                t.x_align_to(
810                                    state.ids.item_frame,
811                                    conrod_core::position::Align::Start,
812                                )
813                                .down_from(state.ids.item_frame, V_PAD)
814                            } else {
815                                t.down_from(state.ids.stats[i - 1], V_PAD_STATS)
816                            }
817                        })
818                        .set(state.ids.stats[i], ui);
819                };
820
821                let mut index = 0;
822
823                if armor_stats.protection.is_some() {
824                    stat_text(
825                        format!(
826                            "{} : {}",
827                            i18n.get_msg("common-stats-armor"),
828                            util::protec2string(
829                                armor_stats.protection.unwrap_or(Protection::Normal(0.0))
830                            )
831                        ),
832                        index,
833                    );
834                    index += 1;
835                }
836
837                // Poise res
838                if armor_stats.poise_resilience.is_some() {
839                    stat_text(
840                        format!(
841                            "{} : {}",
842                            i18n.get_msg("common-stats-poise_res"),
843                            util::protec2string(
844                                armor_stats
845                                    .poise_resilience
846                                    .unwrap_or(Protection::Normal(0.0))
847                            )
848                        ),
849                        index,
850                    );
851                    index += 1;
852                }
853
854                // Max Energy
855                if armor_stats.energy_max.is_some() {
856                    stat_text(
857                        format!(
858                            "{} : {:.1}",
859                            i18n.get_msg("common-stats-energy_max"),
860                            armor_stats.energy_max.unwrap_or(0.0)
861                        ),
862                        index,
863                    );
864                    index += 1;
865                }
866
867                // Energy Recovery
868                if armor_stats.energy_reward.is_some() {
869                    stat_text(
870                        format!(
871                            "{} : {:.1}%",
872                            i18n.get_msg("common-stats-energy_reward"),
873                            armor_stats.energy_reward.map_or(0.0, |x| x * 100.0)
874                        ),
875                        index,
876                    );
877                    index += 1;
878                }
879
880                // Precision Power
881                if armor_stats.precision_power.is_some() {
882                    stat_text(
883                        format!(
884                            "{} : {:.3}",
885                            i18n.get_msg("common-stats-precision_power"),
886                            armor_stats.precision_power.unwrap_or(0.0)
887                        ),
888                        index,
889                    );
890                    index += 1;
891                }
892
893                // Stealth
894                if armor_stats.stealth.is_some() {
895                    stat_text(
896                        format!(
897                            "{} : {:.3}",
898                            i18n.get_msg("common-stats-stealth"),
899                            armor_stats.stealth.unwrap_or(0.0)
900                        ),
901                        index,
902                    );
903                    index += 1;
904                }
905
906                // Slots
907                if item.num_slots() > 0 {
908                    stat_text(
909                        format!(
910                            "{} : {}",
911                            i18n.get_msg("common-stats-slots"),
912                            item.num_slots()
913                        ),
914                        index,
915                    );
916                    index += 1;
917                }
918
919                if let Some(durability) = util::item_durability(item) {
920                    stat_text(
921                        format!(
922                            "{} : {}/{}",
923                            i18n.get_msg("common-stats-durability"),
924                            durability,
925                            Item::MAX_DURABILITY
926                        ),
927                        index,
928                    );
929                }
930
931                if let Some(equipped_item) = equipped_item
932                    && let ItemKind::Armor(equipped_armor) = &*equipped_item.kind()
933                {
934                    let equipped_stats =
935                        equipped_armor.stats(self.msm, equipped_item.stats_durability_multiplier());
936                    let diff = armor_stats - equipped_stats;
937                    let protection_diff = util::option_comparison(
938                        &armor_stats.protection,
939                        &equipped_stats.protection,
940                    );
941                    let poise_res_diff = util::option_comparison(
942                        &armor_stats.poise_resilience,
943                        &equipped_stats.poise_resilience,
944                    );
945                    let energy_max_diff = util::option_comparison(
946                        &armor_stats.energy_max,
947                        &equipped_stats.energy_max,
948                    );
949                    let energy_reward_diff = util::option_comparison(
950                        &armor_stats.energy_reward,
951                        &equipped_stats.energy_reward,
952                    );
953                    let precision_power_diff = util::option_comparison(
954                        &armor_stats.precision_power,
955                        &equipped_stats.precision_power,
956                    );
957                    let stealth_diff =
958                        util::option_comparison(&armor_stats.stealth, &equipped_stats.stealth);
959
960                    let armor_durability = util::item_durability(item);
961                    let equipped_durability = util::item_durability(equipped_item);
962                    let durability_diff =
963                        util::option_comparison(&armor_durability, &equipped_durability);
964
965                    let mut diff_text = |text: String, color, id_index| {
966                        widget::Text::new(&text)
967                            .align_middle_y_of(state.ids.stats[id_index])
968                            .right_from(state.ids.stats[id_index], H_PAD)
969                            .graphics_for(id)
970                            .parent(id)
971                            .with_style(style)
972                            .color(color)
973                            .set(state.ids.diffs[id_index], ui)
974                    };
975
976                    let mut index = 0;
977                    if let Some(p_diff) = diff.protection
978                        && p_diff != Protection::Normal(0.0)
979                    {
980                        let text =
981                            format!("{} {}", &protection_diff.0, util::protec2string(p_diff));
982                        diff_text(text, protection_diff.1, index);
983                    }
984                    index += armor_stats.protection.is_some() as usize;
985
986                    if let Some(p_r_diff) = diff.poise_resilience
987                        && p_r_diff != Protection::Normal(0.0)
988                    {
989                        let text =
990                            format!("{} {}", &poise_res_diff.0, util::protec2string(p_r_diff));
991                        diff_text(text, poise_res_diff.1, index);
992                    }
993                    index += armor_stats.poise_resilience.is_some() as usize;
994
995                    if let Some(e_m_diff) = diff.energy_max
996                        && e_m_diff.abs() > Energy::ENERGY_EPSILON
997                    {
998                        let text = format!("{} {:.1}", &energy_max_diff.0, e_m_diff);
999                        diff_text(text, energy_max_diff.1, index);
1000                    }
1001                    index += armor_stats.energy_max.is_some() as usize;
1002
1003                    if let Some(e_r_diff) = diff.energy_reward
1004                        && e_r_diff.abs() > Energy::ENERGY_EPSILON
1005                    {
1006                        let text = format!("{} {:.1}", &energy_reward_diff.0, e_r_diff * 100.0);
1007                        diff_text(text, energy_reward_diff.1, index);
1008                    }
1009                    index += armor_stats.energy_reward.is_some() as usize;
1010
1011                    if let Some(p_p_diff) = diff.precision_power
1012                        && p_p_diff != 0.0_f32
1013                    {
1014                        let text = format!("{} {:.3}", &precision_power_diff.0, p_p_diff);
1015                        diff_text(text, precision_power_diff.1, index);
1016                    }
1017                    index += armor_stats.precision_power.is_some() as usize;
1018
1019                    if let Some(s_diff) = diff.stealth
1020                        && s_diff != 0.0_f32
1021                    {
1022                        let text = format!("{} {:.3}", &stealth_diff.0, s_diff);
1023                        diff_text(text, stealth_diff.1, index);
1024                    }
1025                    index += armor_stats.stealth.is_some() as usize;
1026
1027                    if armor_durability != equipped_durability && item.has_durability() {
1028                        let diff = armor_durability.unwrap_or(Item::MAX_DURABILITY) as i32
1029                            - equipped_durability.unwrap_or(Item::MAX_DURABILITY) as i32;
1030                        let text = format!("{} {}", &durability_diff.0, diff);
1031                        diff_text(text, durability_diff.1, index);
1032                    }
1033                }
1034            },
1035            ItemKind::Consumable { effects, .. } => {
1036                for (i, desc) in util::consumable_desc(effects, i18n).iter().enumerate() {
1037                    let (down_from, pad) = match i {
1038                        0 => (state.ids.item_frame, V_PAD),
1039                        _ => (state.ids.stats[i - 1], V_PAD_STATS),
1040                    };
1041                    widget::Text::new(desc)
1042                        .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1043                        .graphics_for(id)
1044                        .parent(id)
1045                        .with_style(self.style.desc)
1046                        .color(text_color)
1047                        .down_from(down_from, pad)
1048                        .set(state.ids.stats[i], ui);
1049                }
1050            },
1051            ItemKind::RecipeGroup { recipes } => {
1052                const KNOWN_COLOR: Color = Color::Rgba(0.0, 1.0, 0.0, 1.0);
1053                const NOT_KNOWN_COLOR: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
1054                let recipe_known = self.inventory.map(|inv| {
1055                    inv.recipe_groups_iter()
1056                        .any(|group| group.item_definition_id() == item.item_definition_id())
1057                });
1058                let known_key_color = match recipe_known {
1059                    Some(false) => Some(("items-common-recipe-not_known", NOT_KNOWN_COLOR)),
1060                    Some(true) => Some(("items-common-recipe-known", KNOWN_COLOR)),
1061                    None => None,
1062                };
1063                if let Some((recipe_known_key, recipe_known_color)) = known_key_color {
1064                    let recipe_known_text = self.localized_strings.get_msg(recipe_known_key);
1065                    widget::Text::new(&recipe_known_text)
1066                        .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1067                        .graphics_for(id)
1068                        .parent(id)
1069                        .with_style(self.style.desc)
1070                        .color(recipe_known_color)
1071                        .down_from(state.ids.item_frame, V_PAD)
1072                        .set(state.ids.stats[0], ui);
1073                }
1074                let offset = if known_key_color.is_some() { 1 } else { 0 };
1075                for (i, desc) in recipes.iter().enumerate().map(|(i, s)| (i + offset, s)) {
1076                    if let Some(recipe) = self.rbm.get(desc) {
1077                        let (item_name, _) = util::item_text(
1078                            recipe.output.0.as_ref(),
1079                            self.localized_strings,
1080                            self.item_i18n,
1081                        );
1082                        let (down_from, pad) = match i {
1083                            0 => (state.ids.item_frame, V_PAD),
1084                            _ => (state.ids.stats[i - 1], V_PAD_STATS),
1085                        };
1086                        widget::Text::new(&item_name)
1087                            .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1088                            .graphics_for(id)
1089                            .parent(id)
1090                            .with_style(self.style.desc)
1091                            .color(text_color)
1092                            .down_from(down_from, pad)
1093                            .set(state.ids.stats[i], ui);
1094                    }
1095                }
1096            },
1097            ItemKind::ModularComponent(mc) => {
1098                if let Some(stats) = mc.tool_stats(item.components(), self.msm) {
1099                    let is_primary = matches!(mc, ModularComponent::ToolPrimaryComponent { .. });
1100
1101                    // Power
1102                    let power_text = if is_primary {
1103                        format!(
1104                            "{} : {:.1}",
1105                            i18n.get_msg("common-stats-power"),
1106                            stats.power * 10.0
1107                        )
1108                    } else {
1109                        format!(
1110                            "{} : x{:.2}",
1111                            i18n.get_msg("common-stats-power"),
1112                            stats.power
1113                        )
1114                    };
1115                    widget::Text::new(&power_text)
1116                        .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1117                        .graphics_for(id)
1118                        .parent(id)
1119                        .with_style(self.style.desc)
1120                        .color(text_color)
1121                        .down_from(state.ids.item_frame, V_PAD)
1122                        .set(state.ids.stats[0], ui);
1123
1124                    // Speed
1125                    let speed_text = if is_primary {
1126                        format!(
1127                            "{} : {:+.0}%",
1128                            i18n.get_msg("common-stats-speed"),
1129                            (stats.speed - 1.0) * 100.0
1130                        )
1131                    } else {
1132                        format!(
1133                            "{} : x{:.2}",
1134                            i18n.get_msg("common-stats-speed"),
1135                            stats.speed
1136                        )
1137                    };
1138                    widget::Text::new(&speed_text)
1139                        .graphics_for(id)
1140                        .parent(id)
1141                        .with_style(self.style.desc)
1142                        .color(text_color)
1143                        .down_from(state.ids.stats[0], V_PAD_STATS)
1144                        .set(state.ids.stats[1], ui);
1145
1146                    // Effect Power
1147                    // TODO: Allow effect power to have different terminology based on what it is
1148                    // affecting.
1149                    let effect_power_text = if is_primary {
1150                        format!(
1151                            "{} : {:+.0}%",
1152                            i18n.get_msg("common-stats-effect-power"),
1153                            (stats.effect_power - 1.0) * 100.0
1154                        )
1155                    } else {
1156                        format!(
1157                            "{} : x{:.2}",
1158                            i18n.get_msg("common-stats-effect-power"),
1159                            stats.effect_power
1160                        )
1161                    };
1162                    widget::Text::new(&effect_power_text)
1163                        .graphics_for(id)
1164                        .parent(id)
1165                        .with_style(self.style.desc)
1166                        .color(text_color)
1167                        .down_from(state.ids.stats[1], V_PAD_STATS)
1168                        .set(state.ids.stats[2], ui);
1169
1170                    // Range
1171                    let range_text = if is_primary {
1172                        format!(
1173                            "{} : {:.0}%",
1174                            i18n.get_msg("common-stats-range"),
1175                            (stats.range - 1.0) * 100.0
1176                        )
1177                    } else {
1178                        format!(
1179                            "{} : x{:.2}",
1180                            i18n.get_msg("common-stats-range"),
1181                            stats.range
1182                        )
1183                    };
1184                    widget::Text::new(&range_text)
1185                        .graphics_for(id)
1186                        .parent(id)
1187                        .with_style(self.style.desc)
1188                        .color(text_color)
1189                        .down_from(state.ids.stats[2], V_PAD_STATS)
1190                        .set(state.ids.stats[3], ui);
1191
1192                    // Energy Efficiency
1193                    let energy_eff_text = if is_primary {
1194                        format!(
1195                            "{} : {:.0}%",
1196                            i18n.get_msg("common-stats-energy_efficiency"),
1197                            (stats.energy_efficiency - 1.0) * 100.0
1198                        )
1199                    } else {
1200                        format!(
1201                            "{} : x{:.2}",
1202                            i18n.get_msg("common-stats-energy_efficiency"),
1203                            stats.energy_efficiency
1204                        )
1205                    };
1206                    widget::Text::new(&energy_eff_text)
1207                        .graphics_for(id)
1208                        .parent(id)
1209                        .with_style(self.style.desc)
1210                        .color(text_color)
1211                        .down_from(state.ids.stats[3], V_PAD_STATS)
1212                        .set(state.ids.stats[4], ui);
1213
1214                    // Buff Strength
1215                    let buff_str_text = if is_primary {
1216                        format!(
1217                            "{} : {:.0}%",
1218                            i18n.get_msg("common-stats-buff_strength"),
1219                            (stats.buff_strength - 1.0) * 100.0
1220                        )
1221                    } else {
1222                        format!(
1223                            "{} : x{:.2}",
1224                            i18n.get_msg("common-stats-buff_strength"),
1225                            stats.buff_strength
1226                        )
1227                    };
1228                    widget::Text::new(&buff_str_text)
1229                        .graphics_for(id)
1230                        .parent(id)
1231                        .with_style(self.style.desc)
1232                        .color(text_color)
1233                        .down_from(state.ids.stats[4], V_PAD_STATS)
1234                        .set(state.ids.stats[5], ui);
1235                }
1236            },
1237            _ => (),
1238        }
1239
1240        // Description
1241        if !desc.is_empty() {
1242            widget::Text::new(&format!("\"{}\"", &desc))
1243                .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1244                .graphics_for(id)
1245                .parent(id)
1246                .with_style(self.style.desc)
1247                .color(conrod_core::color::GREY)
1248                .down_from(
1249                    if stats_count > 0 {
1250                        state.ids.stats[state.ids.stats.len() - 1]
1251                    } else {
1252                        state.ids.item_frame
1253                    },
1254                    V_PAD,
1255                )
1256                .w(text_w)
1257                .set(state.ids.desc, ui);
1258        }
1259
1260        // Price display
1261        if let Some((buy, sell, factor)) =
1262            util::price_desc(self.prices, item.item_definition_id(), i18n)
1263        {
1264            widget::Text::new(&buy)
1265                .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1266                .graphics_for(id)
1267                .parent(id)
1268                .with_style(self.style.desc)
1269                .color(Color::Rgba(factor, 1.0 - factor, 0.00, 1.0))
1270                .down_from(
1271                    if !desc.is_empty() {
1272                        state.ids.desc
1273                    } else if stats_count > 0 {
1274                        state.ids.stats[state.ids.stats.len() - 1]
1275                    } else {
1276                        state.ids.item_frame
1277                    },
1278                    V_PAD,
1279                )
1280                .w(text_w)
1281                .set(state.ids.prices_buy, ui);
1282
1283            widget::Text::new(&sell)
1284                .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1285                .graphics_for(id)
1286                .parent(id)
1287                .with_style(self.style.desc)
1288                .color(Color::Rgba(1.0 - factor, factor, 0.00, 1.0))
1289                .down_from(state.ids.prices_buy, V_PAD_STATS)
1290                .w(text_w)
1291                .set(state.ids.prices_sell, ui);
1292
1293            //Tooltips for trade mini-tutorial
1294            widget::Text::new(&format!(
1295                "{}\n{}",
1296                i18n.get_msg("hud-trade-tooltip_hint_1"),
1297                i18n.get_msg("hud-trade-tooltip_hint_2"),
1298            ))
1299            .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1300            .graphics_for(id)
1301            .parent(id)
1302            .with_style(self.style.desc)
1303            .color(Color::Rgba(255.0, 255.0, 255.0, 1.0))
1304            .down_from(state.ids.prices_sell, V_PAD_STATS)
1305            .w(text_w)
1306            .set(state.ids.tooltip_hints, ui);
1307        }
1308    }
1309
1310    /// Default width is based on the description font size unless the text is
1311    /// small enough to fit on a single line
1312    fn default_x_dimension(&self, _ui: &Ui) -> Dimension { Dimension::Absolute(WIDTH) }
1313
1314    fn default_y_dimension(&self, ui: &Ui) -> Dimension {
1315        let item = &self.item;
1316
1317        let (_, desc) = util::item_text(item, self.localized_strings, self.item_i18n);
1318
1319        let (text_w, _image_w) = self.text_image_width(WIDTH);
1320
1321        // Item frame
1322        let frame_h = ICON_SIZE[1] + V_PAD;
1323
1324        // Stats
1325        let stats_count = util::line_count(self.item, self.msm, self.localized_strings);
1326        let stat_h = if stats_count > 0 {
1327            widget::Text::new("placeholder")
1328                .with_style(self.style.desc)
1329                .get_h(ui)
1330                .unwrap_or(0.0)
1331                * stats_count as f64
1332                + (stats_count - 1) as f64 * V_PAD_STATS
1333                + V_PAD
1334        } else {
1335            0.0
1336        };
1337
1338        // Description
1339        let desc_h: f64 = if !desc.is_empty() {
1340            widget::Text::new(&format!("\"{}\"", &desc))
1341                .with_style(self.style.desc)
1342                .w(text_w)
1343                .get_h(ui)
1344                .unwrap_or(0.0)
1345                + V_PAD
1346        } else {
1347            0.0
1348        };
1349
1350        // Price
1351        let price_h: f64 = if let Some((buy, sell, _)) = util::price_desc(
1352            self.prices,
1353            item.item_definition_id(),
1354            self.localized_strings,
1355        ) {
1356            // Get localized tooltip strings (gotten here because these should
1357            // only show if in a trade- aka if buy/sell prices are present)
1358            let tt_hint_1 = self.localized_strings.get_msg("hud-trade-tooltip_hint_1");
1359            let tt_hint_2 = self.localized_strings.get_msg("hud-trade-tooltip_hint_2");
1360
1361            widget::Text::new(&format!("{}\n{}\n{}\n{}", buy, sell, tt_hint_1, tt_hint_2))
1362                .with_style(self.style.desc)
1363                .w(text_w)
1364                .get_h(ui)
1365                .unwrap_or(0.0)
1366                + V_PAD * 2.0
1367        } else {
1368            0.0
1369        };
1370
1371        // extra padding to fit frame top padding
1372        let height = frame_h + stat_h + desc_h + price_h + V_PAD + 5.0;
1373        Dimension::Absolute(height)
1374    }
1375}
1376
1377impl Colorable for ItemTooltip<'_> {
1378    builder_method!(color { style.color = Some(Color) });
1379}