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
43const MOUSE_PAD_Y: f64 = 15.0;
45
46pub struct ItemTooltipManager {
47 state: HoverState,
48 hover_dur: Duration,
50 fade_dur: Duration,
52 logical_scale_factor: f64,
54 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 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 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 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 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
271const V_PAD: f64 = 10.0;
273const H_PAD: f64 = 10.0;
275const V_PAD_STATS: f64 = 6.0;
277const IMAGE_W_FRAC: f64 = 0.3;
279const ICON_SIZE: [f64; 2] = [64.0, 64.0];
281const WIDTH: f64 = 320.0;
283
284#[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: 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 }
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 fn text_image_width(&self, total_width: f64) -> (f64, f64) {
415 let inner_width = (total_width - H_PAD * 2.0).max(0.0);
416 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 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 #[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 let (text_w, image_w) = self.text_image_width(rect.w());
510
511 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 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 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 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 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 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 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 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 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 match &*item.kind() {
624 ItemKind::Tool(tool) => {
625 let stats = tool.stats(item.stats_durability_multiplier());
626
627 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let frame_h = ICON_SIZE[1] + V_PAD;
1323
1324 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 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 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 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 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}