Skip to main content

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