From 763cf70b70604a473f6dee3d4ee9088e06345f5a Mon Sep 17 00:00:00 2001 From: Snowram Date: Tue, 2 Mar 2021 01:45:02 +0100 Subject: [PATCH] New tooltip --- common/src/comp/inventory/item/armor.rs | 47 ++- common/src/comp/inventory/item/tool.rs | 17 +- common/src/comp/inventory/loadout.rs | 12 + common/src/comp/inventory/mod.rs | 6 +- voxygen/src/hud/bag.rs | 30 +- voxygen/src/hud/item_info.rs | 375 +++++++++++++++++ voxygen/src/hud/mod.rs | 30 +- voxygen/src/hud/overitem.rs | 5 +- voxygen/src/hud/skillbar.rs | 26 +- voxygen/src/hud/util.rs | 200 +++++++--- voxygen/src/ui/mod.rs | 24 +- voxygen/src/ui/widgets/item_tooltip.rs | 508 ++++++++++++++++++++++++ voxygen/src/ui/widgets/mod.rs | 1 + 13 files changed, 1209 insertions(+), 72 deletions(-) create mode 100644 voxygen/src/hud/item_info.rs create mode 100644 voxygen/src/ui/widgets/item_tooltip.rs diff --git a/common/src/comp/inventory/item/armor.rs b/common/src/comp/inventory/item/armor.rs index d19e129ee2..9c5f5cf023 100644 --- a/common/src/comp/inventory/item/armor.rs +++ b/common/src/comp/inventory/item/armor.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::{cmp::Ordering, ops::Sub}; #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ArmorKind { @@ -26,12 +27,12 @@ impl Armor { } #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub struct Stats { - protection: Protection, - poise_resilience: Protection, +pub struct Stats { + pub protection: Protection, + pub poise_resilience: Protection, } -impl Stats { +impl Stats { // DO NOT USE UNLESS YOU KNOW WHAT YOU ARE DOING // Added for csv import of stats pub fn new(protection: Protection, poise_resilience: Protection) -> Self { @@ -42,16 +43,52 @@ impl Stats { } } +impl Sub> for Stats { + type Output = Self; + + fn sub(self, other: Self) -> Self::Output { + Self { + protection: self.protection - other.protection, + poise_resilience: self.poise_resilience - other.poise_resilience, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum Protection { Invincible, Normal(f32), } +impl Sub for Protection { + type Output = Self; + + fn sub(self, other: Self) -> Self::Output { + let diff = match (self, other) { + (Protection::Invincible, Protection::Normal(_)) => f32::INFINITY, + (Protection::Invincible, Protection::Invincible) => 0_f32, + (Protection::Normal(_), Protection::Invincible) => -f32::INFINITY, + (Protection::Normal(a), Protection::Normal(b)) => a - b, + }; + Protection::Normal(diff) + } +} + +impl PartialOrd for Protection { + fn partial_cmp(&self, other: &Self) -> Option { + match (*self, *other) { + (Protection::Invincible, Protection::Invincible) => Some(Ordering::Equal), + (Protection::Invincible, _) => Some(Ordering::Less), + (_, Protection::Invincible) => Some(Ordering::Greater), + (Protection::Normal(a), Protection::Normal(b)) => f32::partial_cmp(&a, &b), + } + } +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Armor { pub kind: ArmorKind, - pub stats: Stats, + pub stats: Stats, } impl Armor { diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index bc60eec199..a96ac390f5 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -8,7 +8,7 @@ use crate::{ use hashbrown::HashMap; use serde::{Deserialize, Serialize}; use std::{ - ops::{AddAssign, DivAssign, MulAssign}, + ops::{AddAssign, DivAssign, MulAssign, Sub}, time::Duration, }; use tracing::error; @@ -139,6 +139,21 @@ impl DivAssign for Stats { } } +impl Sub for Stats { + type Output = Self; + + fn sub(self, other: Self) -> Self::Output { + Self { + equip_time_secs: self.equip_time_secs - other.equip_time_secs, + power: self.power - other.power, + poise_strength: self.poise_strength - other.poise_strength, + speed: self.speed - other.speed, + crit_chance: self.crit_chance - other.crit_chance, + crit_mult: self.crit_mult - other.crit_mult, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MaterialStatManifest(pub HashMap); diff --git a/common/src/comp/inventory/loadout.rs b/common/src/comp/inventory/loadout.rs index aaf8581e78..b9318d01cb 100644 --- a/common/src/comp/inventory/loadout.rs +++ b/common/src/comp/inventory/loadout.rs @@ -206,6 +206,18 @@ impl Loadout { .or_else(|| first.map(|x| x.equip_slot)) } + /// Returns all items currently equipped that an item of the given ItemKind + /// could replace + pub(super) fn equipped_items_of_kind( + &self, + item_kind: ItemKind, + ) -> impl Iterator { + self.slots + .iter() + .filter(move |s| s.equip_slot.can_hold(&item_kind)) + .filter_map(|s| s.slot.as_ref()) + } + /// Returns the `InvSlot` for a given `LoadoutSlotId` pub(super) fn inv_slot(&self, loadout_slot_id: LoadoutSlotId) -> Option<&InvSlot> { self.slots diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index 105d3e84ee..0727ac36d5 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -9,7 +9,7 @@ use tracing::{debug, trace, warn}; use crate::{ comp::{ inventory::{ - item::{ItemDef, MaterialStatManifest}, + item::{ItemDef, ItemKind, MaterialStatManifest}, loadout::Loadout, slot::{EquipSlot, Slot, SlotError}, }, @@ -676,6 +676,10 @@ impl Inventory { true } + + pub fn equipped_items_of_kind(&self, item_kind: ItemKind) -> impl Iterator { + self.loadout.equipped_items_of_kind(item_kind) + } } impl Component for Inventory { diff --git a/voxygen/src/hud/bag.rs b/voxygen/src/hud/bag.rs index ec551dbcc8..74418cbe9d 100644 --- a/voxygen/src/hud/bag.rs +++ b/voxygen/src/hud/bag.rs @@ -12,7 +12,7 @@ use crate::{ ui::{ fonts::Fonts, slot::{ContentSize, SlotMaker}, - ImageFrame, Tooltip, TooltipManager, Tooltipable, + ImageFrame, Tooltip, TooltipManager, Tooltipable, ItemTooltip, ItemTooltipManager, ItemTooltipable, }, }; use client::Client; @@ -451,6 +451,7 @@ pub struct Bag<'a> { common: widget::CommonBuilder, rot_imgs: &'a ImgsRot, tooltip_manager: &'a mut TooltipManager, + item_tooltip_manager: &'a mut ItemTooltipManager, slot_manager: &'a mut SlotManager, pulse: f32, localized_strings: &'a Localization, @@ -471,6 +472,7 @@ impl<'a> Bag<'a> { fonts: &'a Fonts, rot_imgs: &'a ImgsRot, tooltip_manager: &'a mut TooltipManager, + item_tooltip_manager: &'a mut ItemTooltipManager, slot_manager: &'a mut SlotManager, pulse: f32, localized_strings: &'a Localization, @@ -489,6 +491,7 @@ impl<'a> Bag<'a> { common: widget::CommonBuilder::default(), rot_imgs, tooltip_manager, + item_tooltip_manager, slot_manager, pulse, localized_strings, @@ -578,6 +581,25 @@ impl<'a> Widget for Bag<'a> { .font_id(self.fonts.cyri.conrod_id) .desc_text_color(TEXT_COLOR); + // Tooltips + let item_tooltip2 = ItemTooltip::new({ + // Edge images [t, b, r, l] + // Corner images [tr, tl, br, bl] + let edge = &self.rot_imgs.tt_side; + let corner = &self.rot_imgs.tt_corner; + ImageFrame::new( + [edge.cw180, edge.none, edge.cw270, edge.cw90], + [corner.none, corner.cw270, corner.cw90, corner.cw180], + Color::Rgba(0.08, 0.07, 0.04, 1.0), + 5.0, + ) + }) + .title_font_size(self.fonts.cyri.scale(15)) + .parent(ui.window) + .desc_font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .desc_text_color(TEXT_COLOR); + InventoryScroller::new( self.client, self.imgs, @@ -816,11 +838,11 @@ impl<'a> Widget for Bag<'a> { .mid_bottom_with_margin_on(state.ids.neck_slot, -95.0) .with_icon(self.imgs.chest_bg, Vec2::new(64.0, 42.0), Some(UI_MAIN)) .filled_slot(filled_slot) - .with_tooltip( - self.tooltip_manager, + .with_item_tooltip( + self.item_tooltip_manager, title, &*desc, - &item_tooltip, + &item_tooltip2, chest_q_col, ) .set(state.ids.chest_slot, ui); diff --git a/voxygen/src/hud/item_info.rs b/voxygen/src/hud/item_info.rs new file mode 100644 index 0000000000..134885fbae --- /dev/null +++ b/voxygen/src/hud/item_info.rs @@ -0,0 +1,375 @@ +use super::{ + img_ids::{Imgs, ImgsRot}, + item_imgs::{animate_by_pulse, ItemImgs, ItemKey::Tool as ToolKey}, + util, +}; + +use crate::{ + hud::get_quality_col, + i18n::Localization, + ui::{fonts::Fonts, ImageFrame, Ingameable, Tooltip, TooltipManager, Tooltipable}, +}; +use client::Client; +use common::{ + combat::{combat_rating, Damage}, + comp::item::{ + armor::{Armor, ArmorKind, Protection}, + tool::{Hands, StatKind, Stats, Tool, ToolKind}, + Item, ItemDesc, ItemKind, MaterialStatManifest, Quality, + }, +}; +use conrod_core::{ + color, + widget::{self, Button, Image, Rectangle, Scrollbar, Text}, + widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, +}; + +widget_ids! { + pub struct Ids { + title, + subtitle, + desc, + stat1, + stat2, + stat3, + diff1, + diff2, + diff3, + item_frame, + item_render, + background, + } +} + +#[derive(WidgetCommon)] +pub struct ItemInfo<'a> { + client: &'a Client, + imgs: &'a Imgs, + item_imgs: &'a ItemImgs, + fonts: &'a Fonts, + pulse: f32, + #[conrod(common_builder)] + common: widget::CommonBuilder, + //rot_imgs: &'a ImgsRot, + //tooltip_manager: &'a mut TooltipManager, + localized_strings: &'a Localization, + item: &'a Item, + msm: &'a MaterialStatManifest, +} + +impl<'a> ItemInfo<'a> { + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 + pub fn new( + client: &'a Client, + imgs: &'a Imgs, + item_imgs: &'a ItemImgs, + fonts: &'a Fonts, + pulse: f32, + //rot_imgs: &'a ImgsRot, + //tooltip_manager: &'a mut TooltipManager, + localized_strings: &'a Localization, + item: &'a Item, + msm: &'a MaterialStatManifest, + ) -> Self { + Self { + client, + imgs, + item_imgs, + fonts, + pulse, + common: widget::CommonBuilder::default(), + //rot_imgs, + //tooltip_manager, + localized_strings, + item, + msm, + } + } +} + +pub struct State { + ids: Ids, +} + +impl<'a> Ingameable for ItemInfo<'a> { + fn prim_count(&self) -> usize { + // Number of conrod primitives contained in the overitem display. + // TODO maybe this could be done automatically? + // - 2 Text for name + // - 0 or 2 Rectangle and Text for button + 4 + match self.item.kind() { + ItemKind::Tool(_) => 3, + ItemKind::Armor(_) => 2, + _ => 0, + } + } +} + +pub enum Event { + //Show(bool), +} + +impl<'a> Widget for ItemInfo<'a> { + type Event = Option; + type State = State; + type Style = (); + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + State { + ids: Ids::new(id_gen), + } + } + + #[allow(clippy::unused_unit)] // TODO: Pending review in #587 + fn style(&self) -> Self::Style { () } + + fn update(self, args: widget::UpdateArgs) -> Self::Event { + let widget::UpdateArgs { state, ui, .. } = args; + let item = self.item; + let _i18n = &self.localized_strings; + + let inventories = self.client.inventories(); + let inventory = match inventories.get(self.client.entity()) { + Some(l) => l, + None => return None, + }; + + let equip_slot = inventory.equipped_items_of_kind(self.item.kind().clone()); + + let (title, desc) = (item.name().to_string(), item.description().to_string()); + + let quality = get_quality_col(self.item); + + let subtitle = util::kind_text(item.kind()); + + let text_color = conrod_core::color::WHITE; + + let art_size = [64.0, 64.0]; + + /*// Apply transparency + let color = style.color(ui.theme()).alpha(self.transparency); + + // Background image frame + self.image_frame + .wh(rect.dim()) + .xy(rect.xy()) + .graphics_for(id) + .parent(id) + .color(color) + .set(state.ids.image_frame, ui);*/ + + widget::Rectangle::fill([310.0, 310.0]) + .color(Color::Rgba(0.0, 0.0, 0.0, 0.98)) + .depth(1.0) + .set(state.ids.background, ui); + + // Icon BG + let quality_col_img = match &item.quality() { + Quality::Low => self.imgs.inv_slot_grey, + Quality::Common => self.imgs.inv_slot, + Quality::Moderate => self.imgs.inv_slot_green, + Quality::High => self.imgs.inv_slot_blue, + Quality::Epic => self.imgs.inv_slot_purple, + Quality::Legendary => self.imgs.inv_slot_gold, + Quality::Artifact => self.imgs.inv_slot_orange, + _ => self.imgs.inv_slot_red, + }; + Image::new(quality_col_img) + .w_h(art_size[0] + 10.0, art_size[1] + 10.0) + .top_left_with_margin_on(state.ids.background, 10.0) + .set(state.ids.item_frame, ui); + + // Icon + Image::new(animate_by_pulse( + &self.item_imgs.img_ids_or_not_found_img(item.into()), + self.pulse, + )) + .color(Some(conrod_core::color::WHITE)) + .wh(art_size) + .middle_of(state.ids.item_frame) + .set(state.ids.item_render, ui); + + // Title + Text::new(&title) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(25)) + .y_align_to(state.ids.item_frame, conrod_core::position::Align::End) + .right_from(state.ids.item_frame, 10.0) + .color(quality) + .depth(2.0) + .set(state.ids.title, ui); + + // Subtitle + Text::new(&subtitle) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .color(conrod_core::color::GREY) + .depth(3.0) + .set(state.ids.subtitle, ui); + + // Stats + match item.kind() { + ItemKind::Tool(tool) => { + let stat1 = tool.base_power(self.msm, item.components()) * 10.0; + let stat2 = tool.base_speed(self.msm, item.components()) * 10.0; + let stat3 = tool.base_poise_strength(self.msm, item.components()) * 10.0; + + Text::new(&format!("Power : {}", stat1.to_string())) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start) + .color(text_color) + .depth(3.0) + .set(state.ids.stat1, ui); + Text::new(&format!("Speed : {}", stat2.to_string())) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .color(text_color) + .depth(3.0) + .set(state.ids.stat2, ui); + Text::new(&format!("Poise : {}", stat3.to_string())) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .color(text_color) + .depth(3.0) + .set(state.ids.stat3, ui); + if let Some(equipped_item) = equip_slot.cloned().next() { + if let ItemKind::Tool(equipped_tool) = equipped_item.kind() { + let tool_stats = tool + .stats + .resolve_stats(self.msm, item.components()) + .clamp_speed(); + let equipped_tool_stats = equipped_tool + .stats + .resolve_stats(self.msm, equipped_item.components()) + .clamp_speed(); + let diff = tool_stats - equipped_tool_stats; + let diff1 = util::comparaison(tool_stats.power, equipped_tool_stats.power); + let diff2 = util::comparaison(tool_stats.speed, equipped_tool_stats.speed); + let diff3 = util::comparaison( + tool_stats.poise_strength, + equipped_tool_stats.poise_strength, + ); + + Text::new(&format!("{} {:.1}", &diff1.0, &diff.power * 10.0)) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .color(diff1.1) + .align_middle_y_of(state.ids.stat1) + .right_from(state.ids.stat1, 10.0) + .depth(3.0) + .set(state.ids.diff1, ui); + Text::new(&format!("{} {:.1}", &diff2.0, &diff.speed * 10.0)) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .color(diff2.1) + .align_middle_y_of(state.ids.stat2) + .right_from(state.ids.stat2, 10.0) + .depth(3.0) + .set(state.ids.diff2, ui); + Text::new(&format!("{} {:.1}", &diff3.0, &diff.poise_strength * 10.0)) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .color(diff3.1) + .align_middle_y_of(state.ids.stat3) + .right_from(state.ids.stat3, 10.0) + .depth(3.0) + .set(state.ids.diff3, ui); + } + } + }, + ItemKind::Armor(armor) => { + let stat1 = armor.get_protection(); + let stat2 = armor.get_poise_resilience(); + + Text::new(&format!("Armour : {}", util::protec2string(stat1))) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start) + .color(text_color) + .depth(3.0) + .set(state.ids.stat1, ui); + Text::new(&format!("Poise res : {}", util::protec2string(stat2))) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .color(text_color) + .depth(3.0) + .set(state.ids.stat2, ui); + + if let Some(equipped_item) = equip_slot.cloned().next() { + if let ItemKind::Armor(equipped_armor) = equipped_item.kind() { + let diff = armor.stats - equipped_armor.stats; + let diff1 = util::comparaison( + &armor.stats.protection, + &equipped_armor.stats.protection, + ); + let diff2 = util::comparaison( + &armor.stats.poise_resilience, + &equipped_armor.stats.poise_resilience, + ); + + Text::new(&format!( + "{} {}", + &diff1.0, + util::protec2string(diff.protection) + )) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .color(diff1.1) + .align_middle_y_of(state.ids.stat1) + .right_from(state.ids.stat1, 10.0) + .depth(3.0) + .set(state.ids.diff1, ui); + Text::new(&format!( + "{} {}", + &diff2.0, + util::protec2string(diff.protection) + )) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .color(diff2.1) + .align_middle_y_of(state.ids.stat2) + .right_from(state.ids.stat2, 10.0) + .depth(3.0) + .set(state.ids.diff2, ui); + } + } + }, + _ => (), + } + + Text::new(&desc) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(15)) + .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start) + .color(conrod_core::color::GREY) + .depth(3.0) + .w(300.0) + .set(state.ids.desc, ui); + + /*let test = widget::Text::new(&desc).w(300.0).get_h(ui); + dbg!(test);*/ + + /*// Items + let stats_count: usize = match item.kind() { + ItemKind::Armor(armor) => 2, + ItemKind::Tool(tool) => 4, + _ => 0, + }; + let gen = &mut ui.widget_id_generator(); + state.update(|state| state.ids.stats.resize(item_count, gen)); + state.update(|state| state.ids.stats_icons.resize(item_count, gen)); + + // Create Stats Widgets + let stats_vec = state + .ids + .stats + .iter() + .copied() + .zip(state.ids.stats_icons.iter().copied()) + .zip(stats) + .collect::>();*/ + + None + } +} diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 87d320f035..6c476125c9 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -9,6 +9,7 @@ mod group; mod hotbar; mod img_ids; mod item_imgs; +mod item_info; mod map; mod minimap; mod overhead; @@ -37,6 +38,7 @@ use esc_menu::EscMenu; use group::Group; use img_ids::Imgs; use item_imgs::ItemImgs; +use item_info::ItemInfo; use map::Map; use minimap::MiniMap; use popup::Popup; @@ -263,6 +265,7 @@ widget_ids! { crafting_window, settings_window, group_window, + item_info, // Free look indicator free_look_txt, @@ -903,7 +906,8 @@ impl Hud { ) -> Vec { span!(_guard, "update_layout", "Hud::update_layout"); let mut events = std::mem::replace(&mut self.events, Vec::new()); - let (ref mut ui_widgets, ref mut tooltip_manager) = self.ui.set_widgets(); + let (ref mut ui_widgets, ref mut item_tooltip_manager, ref mut tooltip_manager) = &mut self.ui.set_widgets(); + //let (ref mut ui_item_widgets, ref mut item_tooltip_manager) = &mut self.ui.set_item_widgets(); // pulse time for pulsating elements self.pulse = self.pulse + dt.as_secs_f32(); // FPS @@ -1338,6 +1342,12 @@ impl Hud { &mut self.ids.overitems, &mut ui_widgets.widget_id_generator(), ); + + let overitem_id2 = overitem_walker.next( + &mut self.ids.overitems, + &mut ui_widgets.widget_id_generator(), + ); + let ingame_pos = pos.0 + Vec3::unit_z() * 1.2; let text = if item.amount() > 1 { @@ -1346,9 +1356,12 @@ impl Hud { item.name().to_string() }; + let quality = get_quality_col(item); + // Item overitem::Overitem::new( &text, + &quality, &distance, &self.fonts, &global_state.settings.controls, @@ -1356,6 +1369,19 @@ impl Hud { .x_y(0.0, 100.0) .position_ingame(ingame_pos) .set(overitem_id, ui_widgets); + + item_info::ItemInfo::new( + client, + &self.imgs, + &self.item_imgs, + &self.fonts, + self.pulse, + i18n, + item, + &msm, + ) + .x_y(0.0, 100.0) + .set(overitem_id2, ui_widgets); } let speech_bubbles = &self.speech_bubbles; @@ -2239,6 +2265,7 @@ impl Hud { //&controller, &self.hotbar, tooltip_manager, + item_tooltip_manager, &mut self.slot_manager, i18n, &ability_map, @@ -2262,6 +2289,7 @@ impl Hud { &self.fonts, &self.rot_imgs, tooltip_manager, + item_tooltip_manager, &mut self.slot_manager, self.pulse, i18n, diff --git a/voxygen/src/hud/overitem.rs b/voxygen/src/hud/overitem.rs index 9093275928..c46ebc0c85 100644 --- a/voxygen/src/hud/overitem.rs +++ b/voxygen/src/hud/overitem.rs @@ -24,6 +24,7 @@ widget_ids! { #[derive(WidgetCommon)] pub struct Overitem<'a> { name: &'a str, + quality: &'a Color, distance_from_player_sqr: &'a f32, fonts: &'a Fonts, controls: &'a ControlSettings, @@ -34,12 +35,14 @@ pub struct Overitem<'a> { impl<'a> Overitem<'a> { pub fn new( name: &'a str, + quality: &'a Color, distance_from_player_sqr: &'a f32, fonts: &'a Fonts, controls: &'a ControlSettings, ) -> Self { Self { name, + quality, distance_from_player_sqr, fonts, controls, @@ -118,7 +121,7 @@ impl<'a> Widget for Overitem<'a> { Text::new(&self.name) .font_id(self.fonts.cyri.conrod_id) .font_size(text_font_size as u32) - .color(text_color) + .color(*self.quality) .x_y(0.0, text_pos_y) .depth(self.distance_from_player_sqr + 3.0) .parent(id) diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index 7b6ff3c53c..a96b8bce69 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -11,7 +11,7 @@ use crate::{ ui::{ fonts::Fonts, slot::{ContentSize, SlotMaker}, - ImageFrame, Tooltip, TooltipManager, Tooltipable, + ImageFrame, Tooltip, TooltipManager, Tooltipable, ItemTooltip, ItemTooltipManager, ItemTooltipable, }, window::GameInput, GlobalState, @@ -142,6 +142,7 @@ pub struct Skillbar<'a> { // controller: &'a ControllerInputs, hotbar: &'a hotbar::State, tooltip_manager: &'a mut TooltipManager, + item_tooltip_manager: &'a mut ItemTooltipManager, slot_manager: &'a mut slots::SlotManager, localized_strings: &'a Localization, pulse: f32, @@ -168,6 +169,7 @@ impl<'a> Skillbar<'a> { // controller: &'a ControllerInputs, hotbar: &'a hotbar::State, tooltip_manager: &'a mut TooltipManager, + item_tooltip_manager: &'a mut ItemTooltipManager, slot_manager: &'a mut slots::SlotManager, localized_strings: &'a Localization, ability_map: &'a AbilityMap, @@ -189,6 +191,7 @@ impl<'a> Skillbar<'a> { // controller, hotbar, tooltip_manager, + item_tooltip_manager, slot_manager, localized_strings, ability_map, @@ -481,6 +484,25 @@ impl<'a> Widget for Skillbar<'a> { .desc_font_size(self.fonts.cyri.scale(12)) .font_id(self.fonts.cyri.conrod_id) .desc_text_color(TEXT_COLOR); + + let item_tooltip2 = ItemTooltip::new({ + // Edge images [t, b, r, l] + // Corner images [tr, tl, br, bl] + let edge = &self.rot_imgs.tt_side; + let corner = &self.rot_imgs.tt_corner; + ImageFrame::new( + [edge.cw180, edge.none, edge.cw270, edge.cw90], + [corner.none, corner.cw270, corner.cw90, corner.cw180], + Color::Rgba(0.08, 0.07, 0.04, 1.0), + 5.0, + ) + }) + .title_font_size(self.fonts.cyri.scale(15)) + .parent(ui.window) + .desc_font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .desc_text_color(TEXT_COLOR); + // Helper let tooltip_text = |slot| { content_source @@ -543,7 +565,7 @@ impl<'a> Widget for Skillbar<'a> { .filled_slot(self.imgs.skillbar_slot) .bottom_left_with_margins_on(state.ids.frame, 0.0, 0.0); if let Some((title, desc)) = tooltip_text(hotbar::Slot::One) { - slot.with_tooltip(self.tooltip_manager, title, desc, &item_tooltip, TEXT_COLOR) + slot.with_item_tooltip(self.item_tooltip_manager, title, desc, &item_tooltip2, TEXT_COLOR) .set(state.ids.slot1, ui); } else { slot.set(state.ids.slot1, ui); diff --git a/voxygen/src/hud/util.rs b/voxygen/src/hud/util.rs index d4e48ada12..6dcc033638 100644 --- a/voxygen/src/hud/util.rs +++ b/voxygen/src/hud/util.rs @@ -28,7 +28,7 @@ pub fn loadout_slot_text<'a>( } pub fn item_text<'a>( - item: &'a impl ItemDesc, + item: &'a dyn ItemDesc, msm: &'a MaterialStatManifest, ) -> (&'a str, Cow<'a, str>) { let desc: Cow = match item.kind() { @@ -47,18 +47,18 @@ pub fn item_text<'a>( &msm, item.description(), )), - ItemKind::Glider(_glider) => Cow::Owned(glider_desc(item.description())), + ItemKind::Glider(_glider) => Cow::Owned(generic_desc(item)), ItemKind::Consumable { effect, .. } => { Cow::Owned(consumable_desc(effect, item.description())) }, - ItemKind::Throwable { .. } => Cow::Owned(throwable_desc(item.description())), - ItemKind::Utility { .. } => Cow::Owned(utility_desc(item.description())), + ItemKind::Throwable { .. } => Cow::Owned(generic_desc(item)), + ItemKind::Utility { .. } => Cow::Owned(generic_desc(item)), ItemKind::Ingredient { .. } => Cow::Owned(ingredient_desc( item.description(), item.item_definition_id(), msm, )), - ItemKind::Lantern { .. } => Cow::Owned(lantern_desc(item.description())), + ItemKind::Lantern { .. } => Cow::Owned(generic_desc(item)), ItemKind::TagExamples { .. } => Cow::Borrowed(item.description()), //_ => Cow::Borrowed(item.description()), }; @@ -80,6 +80,46 @@ pub fn append_price_desc(desc: &mut String, prices: &Option, item_de } } +fn use_text(kind: &ItemKind) -> String { + let text = match kind { + ItemKind::Armor(_) + | ItemKind::Tool(_) + | ItemKind::ModularComponent(_) + | ItemKind::Glider(_) + | ItemKind::Consumable { .. } + | ItemKind::Utility { .. } + | ItemKind::Ingredient { .. } + | ItemKind::Lantern { .. } => "", + ItemKind::Throwable { .. } => "", + ItemKind::TagExamples { .. } => "", + }; + text.to_string() +} + +pub fn kind_text(kind: &ItemKind) -> String { + match kind { + ItemKind::Armor(armor) => format!("Armor ({})", armor_kind(&armor)), + ItemKind::Tool(tool) => format!("{} {}", tool_hands(&tool), tool_kind(&tool)), + ItemKind::ModularComponent(_mc) => "Modular Component".to_string(), + ItemKind::Glider(_glider) => "Glider".to_string(), + ItemKind::Consumable { .. } => "Consumable".to_string(), + ItemKind::Throwable { .. } => "Can be thrown".to_string(), + ItemKind::Utility { .. } => "Utility".to_string(), + ItemKind::Ingredient { .. } => "Ingredient".to_string(), + ItemKind::Lantern { .. } => "Lantern".to_string(), + ItemKind::TagExamples { .. } => "".to_string(), + } +} + +fn generic_desc(desc: &dyn ItemDesc) -> String { + format!( + "{}\n\n{}\n\n{}", + kind_text(desc.kind()), + desc.description(), + use_text(desc.kind()) + ) +} + // TODO: localization fn modular_component_desc( mc: &ModularComponent, @@ -100,7 +140,6 @@ fn modular_component_desc( } result } -fn glider_desc(desc: &str) -> String { format!("Glider\n\n{}\n\n", desc) } fn consumable_desc(effects: &[Effect], desc: &str) -> String { // TODO: localization @@ -165,12 +204,6 @@ fn consumable_desc(effects: &[Effect], desc: &str) -> String { description } -fn throwable_desc(desc: &str) -> String { - format!("Can be thrown\n\n{}\n\n", desc) -} - -fn utility_desc(desc: &str) -> String { format!("{}\n\n", desc) } - fn ingredient_desc(desc: &str, item_id: &str, msm: &MaterialStatManifest) -> String { let mut result = format!("Crafting Ingredient\n\n{}", desc); if let Some(stats) = msm.0.get(item_id) { @@ -180,10 +213,9 @@ fn ingredient_desc(desc: &str, item_id: &str, msm: &MaterialStatManifest) -> Str result } -fn lantern_desc(desc: &str) -> String { format!("Lantern\n\n{}\n\n", desc) } +// Armor -fn armor_desc(armor: &Armor, desc: &str, slots: u16) -> String { - // TODO: localization +fn armor_kind(armor: &Armor) -> String { let kind = match armor.kind { ArmorKind::Shoulder(_) => "Shoulders", ArmorKind::Chest(_) => "Chest", @@ -198,33 +230,45 @@ fn armor_desc(armor: &Armor, desc: &str, slots: u16) -> String { ArmorKind::Tabard(_) => "Tabard", ArmorKind::Bag(_) => "Bag", }; - let armor_protection = match armor.get_protection() { - Protection::Normal(a) => a.to_string(), - Protection::Invincible => "Inf".to_string(), - }; - let armor_poise_resilience = match armor.get_poise_resilience() { - Protection::Normal(a) => a.to_string(), - Protection::Invincible => "Inf".to_string(), - }; - - let mut description = format!( - "{}\n\nArmor: {}\n\nPoise Resilience: {}", - kind, armor_protection, armor_poise_resilience - ); - - if !desc.is_empty() { - write!(&mut description, "\n\n{}", desc).unwrap(); - } - - if slots > 0 { - write!(&mut description, "\n\nSlots: {}", slots).unwrap(); - } - - write!(&mut description, "\n\n").unwrap(); - description + kind.to_string() } -fn tool_desc(tool: &Tool, components: &[Item], msm: &MaterialStatManifest, desc: &str) -> String { +fn armor_protection(armor: &Armor) -> String { + match armor.get_protection() { + Protection::Normal(a) => format!("Protection: {}", a.to_string()), + Protection::Invincible => "Protection: Inf".to_string(), + } +} + +pub fn armor_desc(armor: &Armor, desc: &str, slots: u16) -> String { + // TODO: localization + let kind = armor_kind(armor); + let armor_protection = armor_protection(armor); + //let armor_poise_resilience = match armor.get_poise_resilience() { + // Protection::Normal(a) => a.to_string(), + // Protection::Invincible => "Inf".to_string(), + //}; + + let mut desctext: String = "".to_string(); + if !desc.is_empty() { + desctext = desc.to_string(); + } + + let mut slottext: String = "".to_string(); + if slots > 0 { + slottext = format!("Slots: {}", slots) + } + + let usetext = use_text(&ItemKind::Armor(armor.clone())); + format!( + "{} {}\n\n{}\n{}\n{}", + kind, armor_protection, slottext, desctext, usetext + ) +} + +//Tool + +pub fn tool_kind(tool: &Tool) -> String { let kind = match tool.kind { ToolKind::Sword => "Sword", ToolKind::Axe => "Axe", @@ -246,35 +290,63 @@ fn tool_desc(tool: &Tool, components: &[Item], msm: &MaterialStatManifest, desc: ToolKind::Pick => "Pickaxe", ToolKind::Empty => "Empty", }; + kind.to_string() +} - // Get tool stats +pub fn tool_stats(tool: &Tool, components: &[Item], msm: &MaterialStatManifest) -> String { let stats = tool.stats.resolve_stats(msm, components).clamp_speed(); + statblock_desc(&stats) +} +pub fn tool_hands(tool: &Tool) -> String { let hands = match tool.hands { - Hands::One => "One", - Hands::Two => "Two", + Hands::One => "One-Handed", + Hands::Two => "Two-Handed", }; + hands.to_string() +} - let mut result = format!("{}-Handed {}\n\n", hands, kind); - result += &statblock_desc(&stats); +fn components_list(components: &[Item]) -> String { + let mut text: String = "Made from:\n".to_string(); + for component in components { + text += component.name(); + text += "\n" + } + text +} + +pub fn tool_desc( + tool: &Tool, + components: &[Item], + msm: &MaterialStatManifest, + desc: &str, +) -> String { + let kind = tool_kind(tool); + //let poise_strength = tool.base_poise_strength(); + let hands = tool_hands(tool); + let stats = tool_stats(tool, components, msm); + let usetext = use_text(&ItemKind::Tool(tool.clone())); + let mut componentstext: String = "".to_string(); if !components.is_empty() { - result += "Made from:\n"; - for component in components { - result += component.name(); - result += "\n" - } - result += "\n"; + componentstext = components_list(components); } + let mut desctext: String = "".to_string(); if !desc.is_empty() { - result += &format!("{}\n\n", desc); + desctext = desc.to_string(); } - result += ""; - result + format!( + "{} {}\n\n{}\n{}\n{}\n{}", + hands, kind, stats, componentstext, desctext, usetext + ) } fn statblock_desc(stats: &Stats) -> String { format!( - "Power: {:0.1}\n\nPoise Strength: {:0.1}\n\nSpeed: {:0.1}\n\n", + "DPS: {:0.1}\nPower: {:0.1}\nSpeed: {:0.1}\n", + // add back when ready for poise + //"{}\n\nDPS: {:0.1}\n\nPower: {:0.1}\n\nPoise Strength: {:0.1}\n\nSpeed: \ + // {:0.1}\n\n{}\n\n", + stats.speed * stats.power * 10.0, // Damage per second stats.power * 10.0, stats.poise_strength * 10.0, stats.speed, @@ -285,6 +357,24 @@ fn statblock_desc(stats: &Stats) -> String { ) } +// Compare two type, output a colored character to show comparison +pub fn comparaison(first: T, other: T) -> (String, conrod_core::color::Color) { + if first == other { + (".".to_string(), conrod_core::color::GREY) + } else if other < first { + ("^".to_string(), conrod_core::color::GREEN) + } else { + ("v".to_string(), conrod_core::color::RED) + } +} + +pub fn protec2string(stat: Protection) -> String { + match stat { + Protection::Normal(a) => format!("{:.1}", a), + Protection::Invincible => "Infinite".to_string(), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index aef6491f58..eb31769dbe 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -20,6 +20,7 @@ pub use widgets::{ slot, toggle_button::ToggleButton, tooltip::{Tooltip, TooltipManager, Tooltipable}, + item_tooltip::{ItemTooltip, ItemTooltipManager, ItemTooltipable}, }; use crate::{ @@ -124,6 +125,8 @@ pub struct Ui { scale: Scale, // Tooltips tooltip_manager: TooltipManager, + // Item tooltips manager + item_tooltip_manager: ItemTooltipManager, } impl Ui { @@ -138,6 +141,14 @@ impl Ui { // to be updated, there's no reason to set the redraw count higher than // 1. ui.set_num_redraw_frames(1); + + let item_tooltip_manager = ItemTooltipManager::new( + ui.widget_id_generator(), + Duration::from_millis(1), + Duration::from_millis(0), + scale.scale_factor_logical(), + ); + let tooltip_manager = TooltipManager::new( ui.widget_id_generator(), Duration::from_millis(1), @@ -160,6 +171,7 @@ impl Ui { need_cache_resize: false, scale, tooltip_manager, + item_tooltip_manager, }) } @@ -223,8 +235,12 @@ impl Ui { pub fn id_generator(&mut self) -> Generator { self.ui.widget_id_generator() } - pub fn set_widgets(&mut self) -> (UiCell, &mut TooltipManager) { - (self.ui.set_widgets(), &mut self.tooltip_manager) + pub fn set_widgets(&mut self) -> (UiCell, &mut ItemTooltipManager, &mut TooltipManager) { + (self.ui.set_widgets(), &mut self.item_tooltip_manager, &mut self.tooltip_manager) + } + + pub fn set_item_widgets(&mut self) -> (UiCell, &mut ItemTooltipManager) { + (self.ui.set_widgets(), &mut self.item_tooltip_manager) } // Accepts Option so widget can be unfocused. @@ -292,6 +308,10 @@ impl Ui { self.tooltip_manager .maintain(self.ui.global_input(), self.scale.scale_factor_logical()); + // Maintain tooltip manager + self.item_tooltip_manager + .maintain(self.ui.global_input(), self.scale.scale_factor_logical()); + // Handle scale factor changing let need_resize = if let Some(scale_factor) = self.scale_factor_changed.take() { self.scale.scale_factor_changed(scale_factor) diff --git a/voxygen/src/ui/widgets/item_tooltip.rs b/voxygen/src/ui/widgets/item_tooltip.rs new file mode 100644 index 0000000000..c41ac3a430 --- /dev/null +++ b/voxygen/src/ui/widgets/item_tooltip.rs @@ -0,0 +1,508 @@ +use super::image_frame::ImageFrame; +use conrod_core::{ + builder_method, builder_methods, image, input::global::Global, position::Dimension, text, + widget, widget_ids, Color, Colorable, FontSize, Positionable, Sizeable, Ui, UiCell, Widget, + WidgetCommon, WidgetStyle, +}; +use std::time::{Duration, Instant}; +#[derive(Copy, Clone)] +struct Hover(widget::Id, [f64; 2]); +#[derive(Copy, Clone)] +enum HoverState { + Hovering(Hover), + Fading(Instant, Hover, Option<(Instant, widget::Id)>), + Start(Instant, widget::Id), + None, +} + +// Spacing between the tooltip and mouse +const MOUSE_PAD_Y: f64 = 15.0; +const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); // Default text color + +pub struct ItemTooltipManager { + tooltip_id: widget::Id, + state: HoverState, + // How long before a tooltip is displayed when hovering + hover_dur: Duration, + // How long it takes a tooltip to disappear + fade_dur: Duration, + // Current scaling of the ui + logical_scale_factor: f64, +} +impl ItemTooltipManager { + pub fn new( + mut generator: widget::id::Generator, + hover_dur: Duration, + fade_dur: Duration, + logical_scale_factor: f64, + ) -> Self { + Self { + tooltip_id: generator.next(), + state: HoverState::None, + hover_dur, + fade_dur, + logical_scale_factor, + } + } + + pub fn maintain(&mut self, input: &Global, logical_scale_factor: f64) { + self.logical_scale_factor = logical_scale_factor; + + let current = &input.current; + + if let Some(um_id) = current.widget_under_mouse { + match self.state { + HoverState::Hovering(hover) if um_id == hover.0 => (), + HoverState::Hovering(hover) => { + self.state = + HoverState::Fading(Instant::now(), hover, Some((Instant::now(), um_id))) + }, + HoverState::Fading(_, _, Some((_, id))) if um_id == id => {}, + HoverState::Fading(start, hover, _) => { + self.state = HoverState::Fading(start, hover, Some((Instant::now(), um_id))) + }, + HoverState::Start(_, id) if um_id == id => (), + HoverState::Start(_, _) | HoverState::None => { + self.state = HoverState::Start(Instant::now(), um_id) + }, + } + } else { + match self.state { + HoverState::Hovering(hover) => { + self.state = HoverState::Fading(Instant::now(), hover, None) + }, + HoverState::Fading(start, hover, Some((_, _))) => { + self.state = HoverState::Fading(start, hover, None) + }, + HoverState::Start(_, _) => self.state = HoverState::None, + HoverState::Fading(_, _, None) | HoverState::None => (), + } + } + + // Handle fade timing + if let HoverState::Fading(start, _, maybe_hover) = self.state { + if start.elapsed() > self.fade_dur { + self.state = match maybe_hover { + Some((start, hover)) => HoverState::Start(start, hover), + None => HoverState::None, + }; + } + } + } + + #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 + fn set_tooltip( + &mut self, + tooltip: &ItemTooltip, + title_text: &str, + desc_text: &str, + title_col: Color, + img_id: Option, + image_dims: Option<(f64, f64)>, + src_id: widget::Id, + ui: &mut UiCell, + ) { + let tooltip_id = self.tooltip_id; + let mp_h = MOUSE_PAD_Y / self.logical_scale_factor; + + let tooltip = |transparency, mouse_pos: [f64; 2], ui: &mut UiCell| { + // Fill in text and the potential image beforehand to get an accurate size for + // spacing + let tooltip = tooltip + .clone() + .title(title_text) + .desc(desc_text) + .title_col(title_col) + .image(img_id) + .image_dims(image_dims); + + let [t_w, t_h] = tooltip.get_wh(ui).unwrap_or([0.0, 0.0]); + let [m_x, m_y] = [mouse_pos[0], mouse_pos[1]]; + let (w_w, w_h) = (ui.win_w, ui.win_h); + + // Determine position based on size and mouse position + // Flow to the top left of the mouse when there is space + let x = if (m_x + w_w / 2.0) > t_w { + m_x - t_w / 2.0 + } else { + m_x + t_w / 2.0 + }; + let y = if w_h - (m_y + w_h / 2.0) > t_h + mp_h { + m_y + mp_h + t_h / 2.0 + } else { + m_y - mp_h - t_h / 2.0 + }; + tooltip + .floating(true) + .transparency(transparency) + .x_y(x, y) + .set(tooltip_id, ui); + }; + + match self.state { + HoverState::Hovering(Hover(id, xy)) if id == src_id => tooltip(1.0, xy, ui), + HoverState::Fading(start, Hover(id, xy), _) if id == src_id => tooltip( + (0.1f32 - start.elapsed().as_millis() as f32 / self.hover_dur.as_millis() as f32) + .max(0.0), + xy, + ui, + ), + HoverState::Start(start, id) if id == src_id && start.elapsed() > self.hover_dur => { + let xy = ui.global_input().current.mouse.xy; + self.state = HoverState::Hovering(Hover(id, xy)); + tooltip(1.0, xy, ui); + }, + _ => (), + } + } +} + +pub struct ItemTooltipped<'a, W> { + inner: W, + tooltip_manager: &'a mut ItemTooltipManager, + title_text: &'a str, + desc_text: &'a str, + img_id: Option, + image_dims: Option<(f64, f64)>, + tooltip: &'a ItemTooltip<'a>, + title_col: Color, +} +impl<'a, W: Widget> ItemTooltipped<'a, W> { + pub fn tooltip_image(mut self, img_id: image::Id) -> Self { + self.img_id = Some(img_id); + self + } + + pub fn tooltip_image_dims(mut self, dims: (f64, f64)) -> Self { + self.image_dims = Some(dims); + self + } + + pub fn set(self, id: widget::Id, ui: &mut UiCell) -> W::Event { + let event = self.inner.set(id, ui); + self.tooltip_manager.set_tooltip( + self.tooltip, + self.title_text, + self.desc_text, + self.title_col, + self.img_id, + self.image_dims, + id, + ui, + ); + event + } +} + +pub trait ItemTooltipable { + // If `Tooltip` is expensive to construct accept a closure here instead. + fn with_item_tooltip<'a>( + self, + tooltip_manager: &'a mut ItemTooltipManager, + title_text: &'a str, + desc_text: &'a str, + tooltip: &'a ItemTooltip<'a>, + title_col: Color, + ) -> ItemTooltipped<'a, Self> + where + Self: std::marker::Sized; +} +impl ItemTooltipable for W { + fn with_item_tooltip<'a>( + self, + tooltip_manager: &'a mut ItemTooltipManager, + title_text: &'a str, + desc_text: &'a str, + tooltip: &'a ItemTooltip<'a>, + title_col: Color, + ) -> ItemTooltipped<'a, W> { + ItemTooltipped { + inner: self, + tooltip_manager, + title_text, + desc_text, + img_id: None, + image_dims: None, + tooltip, + title_col, + } + } +} + +/// Vertical spacing between elements of the tooltip +const V_PAD: f64 = 10.0; +/// Horizontal spacing between elements of the tooltip +const H_PAD: f64 = 10.0; +/// Default portion of inner width that goes to an image +const IMAGE_W_FRAC: f64 = 0.3; +/// Default width multiplied by the description font size +const DEFAULT_CHAR_W: f64 = 30.0; +/// Text vertical spacing factor to account for overhanging text +const TEXT_SPACE_FACTOR: f64 = 0.35; + +/// A widget for displaying tooltips +#[derive(Clone, WidgetCommon)] +pub struct ItemTooltip<'a> { + #[conrod(common_builder)] + common: widget::CommonBuilder, + title_text: &'a str, + desc_text: &'a str, + title_col: Color, + image: Option, + image_dims: Option<(f64, f64)>, + style: Style, + transparency: f32, + image_frame: ImageFrame, +} + +#[derive(Clone, Debug, Default, PartialEq, WidgetStyle)] +pub struct Style { + #[conrod(default = "Color::Rgba(1.0, 1.0, 1.0, 1.0)")] + pub color: Option, + title: widget::text::Style, + desc: widget::text::Style, + // add background imgs here +} + +widget_ids! { + struct Ids { + title, + desc, + image_frame, + image, + } +} + +pub struct State { + ids: Ids, +} + +impl<'a> ItemTooltip<'a> { + builder_methods! { + pub desc_text_color { style.desc.color = Some(Color) } + pub title_font_size { style.title.font_size = Some(FontSize) } + pub desc_font_size { style.desc.font_size = Some(FontSize) } + pub title_justify { style.title.justify = Some(text::Justify) } + pub desc_justify { style.desc.justify = Some(text::Justify) } + image { image = Option } + title { title_text = &'a str } + desc { desc_text = &'a str } + image_dims { image_dims = Option<(f64, f64)> } + transparency { transparency = f32 } + title_col { title_col = Color} + } + + pub fn new(image_frame: ImageFrame) -> Self { + ItemTooltip { + common: widget::CommonBuilder::default(), + style: Style::default(), + title_text: "", + desc_text: "", + transparency: 1.0, + image_frame, + image: None, + image_dims: None, + title_col: TEXT_COLOR, + } + } + + /// Align the text to the left of its bounding **Rect**'s *x* axis range. + //pub fn left_justify(self) -> Self { + // self.justify(text::Justify::Left) + //} + + /// Align the text to the middle of its bounding **Rect**'s *x* axis range. + //pub fn center_justify(self) -> Self { + // self.justify(text::Justify::Center) + //} + + /// Align the text to the right of its bounding **Rect**'s *x* axis range. + //pub fn right_justify(self) -> Self { + // self.justify(text::Justify::Right) + //} + + fn text_image_width(&self, total_width: f64) -> (f64, f64) { + let inner_width = (total_width - H_PAD * 2.0).max(0.0); + // Image defaults to 30% of the width + let image_w = if self.image.is_some() { + match self.image_dims { + Some((w, _)) => w, + None => (inner_width - H_PAD).max(0.0) * IMAGE_W_FRAC, + } + } else { + 0.0 + }; + // Text gets the remaining width + let text_w = (inner_width + - if self.image.is_some() { + image_w + H_PAD + } else { + 0.0 + }) + .max(0.0); + + (text_w, image_w) + } + + /// Specify the font used for displaying the text. + pub fn font_id(mut self, font_id: text::font::Id) -> Self { + self.style.title.font_id = Some(Some(font_id)); + self.style.desc.font_id = Some(Some(font_id)); + self + } +} + +impl<'a> Widget for ItemTooltip<'a> { + type Event = (); + type State = State; + type Style = Style; + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + State { + ids: Ids::new(id_gen), + } + } + + fn style(&self) -> Self::Style { self.style.clone() } + + fn update(self, args: widget::UpdateArgs) { + let widget::UpdateArgs { + id, + state, + rect, + style, + ui, + .. + } = args; + + // Widths + let (text_w, image_w) = self.text_image_width(rect.w()); + + // Apply transparency + let color = style.color(ui.theme()).alpha(self.transparency); + + // Background image frame + self.image_frame + .wh(rect.dim()) + .xy(rect.xy()) + .graphics_for(id) + .parent(id) + .color(color) + .set(state.ids.image_frame, ui); + + // Image + if let Some(img_id) = self.image { + widget::Image::new(img_id) + .w_h(image_w, self.image_dims.map_or(image_w, |(_, h)| h)) + .graphics_for(id) + .parent(id) + .color(Some(color)) + .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD) + .set(state.ids.image, ui); + } + + // Spacing for overhanging text + let title_space = self.style.title.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR; + + // Title of tooltip + if !self.title_text.is_empty() { + let title = widget::Text::new(self.title_text) + .w(text_w) + .graphics_for(id) + .parent(id) + .with_style(self.style.title) + // Apply transparency + .color(self.title_col); + + if self.image.is_some() { + title + .right_from(state.ids.image, H_PAD) + .align_top_of(state.ids.image) + } else { + title.top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD) + } + .set(state.ids.title, ui); + } + + // Description of tooltip + let desc = widget::Text::new(self.desc_text) + .w(text_w) + .graphics_for(id) + .parent(id) + // Apply transparency + .color(style.desc.color(ui.theme()).alpha(self.transparency)) + .with_style(self.style.desc); + + if !self.title_text.is_empty() { + desc.down_from(state.ids.title, V_PAD * 0.5 + title_space) + .align_left_of(state.ids.title) + } else if self.image.is_some() { + desc.right_from(state.ids.image, H_PAD) + .align_top_of(state.ids.image) + } else { + desc.top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD) + } + .set(state.ids.desc, ui); + } + + /// Default width is based on the description font size unless the text is + /// small enough to fit on a single line + fn default_x_dimension(&self, ui: &Ui) -> Dimension { + let single_line_title_w = widget::Text::new(self.title_text) + .with_style(self.style.title) + .get_w(ui) + .unwrap_or(0.0); + let single_line_desc_w = widget::Text::new(self.desc_text) + .with_style(self.style.desc) + .get_w(ui) + .unwrap_or(0.0); + + let text_w = single_line_title_w.max(single_line_desc_w); + let inner_w = if self.image.is_some() { + match self.image_dims { + Some((w, _)) => w + text_w + H_PAD, + None => text_w / (1.0 - IMAGE_W_FRAC) + H_PAD, + } + } else { + text_w + }; + + let width = + inner_w.min(self.style.desc.font_size(&ui.theme) as f64 * DEFAULT_CHAR_W) + 2.0 * H_PAD; + Dimension::Absolute(width) + } + + fn default_y_dimension(&self, ui: &Ui) -> Dimension { + let (text_w, image_w) = self.text_image_width(self.get_w(ui).unwrap_or(0.0)); + let title_h = if self.title_text.is_empty() { + 0.0 + } else { + widget::Text::new(self.title_text) + .with_style(self.style.title) + .w(text_w) + .get_h(ui) + .unwrap_or(0.0) + + self.style.title.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR + + 0.5 * V_PAD + }; + let desc_h = if self.desc_text.is_empty() { + 0.0 + } else { + widget::Text::new(self.desc_text) + .with_style(self.style.desc) + .w(text_w) + .get_h(ui) + .unwrap_or(0.0) + + self.style.desc.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR + }; + // Image defaults to square shape + let image_h = self.image_dims.map_or(image_w, |(_, h)| h); + // Title height + desc height + padding/spacing + let height = (title_h + desc_h).max(image_h) + 2.0 * V_PAD; + Dimension::Absolute(height) + } +} + +impl<'a> Colorable for ItemTooltip<'a> { + builder_method!(color { style.color = Some(Color) }); +} \ No newline at end of file diff --git a/voxygen/src/ui/widgets/mod.rs b/voxygen/src/ui/widgets/mod.rs index b27ecd39e7..4e74ebd864 100644 --- a/voxygen/src/ui/widgets/mod.rs +++ b/voxygen/src/ui/widgets/mod.rs @@ -6,3 +6,4 @@ pub mod radio_list; pub mod slot; pub mod toggle_button; pub mod tooltip; +pub mod item_tooltip; \ No newline at end of file