mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
395 lines
14 KiB
Rust
395 lines
14 KiB
Rust
use super::{
|
|
animate_by_pulse, get_quality_col,
|
|
img_ids::{Imgs, ImgsRot},
|
|
item_imgs::ItemImgs,
|
|
Show, Windows, TEXT_COLOR,
|
|
};
|
|
use crate::ui::{fonts::Fonts, ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable};
|
|
use client::Client;
|
|
use common::comp::inventory::item::{ItemDef, ItemDesc, MaterialStatManifest, Quality};
|
|
use conrod_core::{
|
|
color,
|
|
position::Dimension,
|
|
widget::{self, Image, List, Rectangle, Scrollbar, Text},
|
|
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
|
|
};
|
|
use i18n::Localization;
|
|
use std::{collections::VecDeque, sync::Arc};
|
|
|
|
widget_ids! {
|
|
struct Ids{
|
|
frame,
|
|
message_box,
|
|
scrollbar,
|
|
message_icons[],
|
|
message_icon_bgs[],
|
|
message_icon_frames[],
|
|
message_texts[],
|
|
message_text_shadows[],
|
|
}
|
|
}
|
|
|
|
const MAX_MESSAGES: usize = 50;
|
|
|
|
const BOX_WIDTH: f64 = 300.0;
|
|
const BOX_HEIGHT: f64 = 350.0;
|
|
|
|
const ICON_BG_SIZE: f64 = 33.0;
|
|
const ICON_SIZE: f64 = 30.0;
|
|
const ICON_LABEL_SPACER: f64 = 7.0;
|
|
|
|
const MESSAGE_VERTICAL_PADDING: f64 = 1.0;
|
|
|
|
const HOVER_FADE_OUT_TIME: f32 = 2.0;
|
|
const MESSAGE_FADE_OUT_TIME: f32 = 4.5;
|
|
const AUTO_SHOW_FADE_OUT_TIME: f32 = 1.0;
|
|
|
|
const MAX_MERGE_TIME: f32 = MESSAGE_FADE_OUT_TIME;
|
|
|
|
#[derive(WidgetCommon)]
|
|
pub struct LootScroller<'a> {
|
|
new_messages: &'a mut VecDeque<LootMessage>,
|
|
|
|
client: &'a Client,
|
|
show: &'a Show,
|
|
imgs: &'a Imgs,
|
|
item_imgs: &'a ItemImgs,
|
|
rot_imgs: &'a ImgsRot,
|
|
fonts: &'a Fonts,
|
|
localized_strings: &'a Localization,
|
|
msm: &'a MaterialStatManifest,
|
|
item_tooltip_manager: &'a mut ItemTooltipManager,
|
|
pulse: f32,
|
|
|
|
#[conrod(common_builder)]
|
|
common: widget::CommonBuilder,
|
|
}
|
|
impl<'a> LootScroller<'a> {
|
|
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
|
|
pub fn new(
|
|
new_messages: &'a mut VecDeque<LootMessage>,
|
|
client: &'a Client,
|
|
show: &'a Show,
|
|
imgs: &'a Imgs,
|
|
item_imgs: &'a ItemImgs,
|
|
rot_imgs: &'a ImgsRot,
|
|
fonts: &'a Fonts,
|
|
localized_strings: &'a Localization,
|
|
msm: &'a MaterialStatManifest,
|
|
item_tooltip_manager: &'a mut ItemTooltipManager,
|
|
pulse: f32,
|
|
) -> Self {
|
|
Self {
|
|
new_messages,
|
|
client,
|
|
show,
|
|
imgs,
|
|
item_imgs,
|
|
rot_imgs,
|
|
fonts,
|
|
localized_strings,
|
|
msm,
|
|
item_tooltip_manager,
|
|
pulse,
|
|
common: widget::CommonBuilder::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct LootMessage {
|
|
pub item: Arc<ItemDef>,
|
|
pub amount: u32,
|
|
}
|
|
|
|
pub struct State {
|
|
ids: Ids,
|
|
messages: VecDeque<(LootMessage, f32)>, // (message, timestamp)
|
|
|
|
last_hover_pulse: Option<f32>,
|
|
last_auto_show_pulse: Option<f32>, // auto show if (for example) bag is open
|
|
}
|
|
|
|
impl<'a> Widget for LootScroller<'a> {
|
|
type Event = ();
|
|
type State = State;
|
|
type Style = ();
|
|
|
|
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
|
|
State {
|
|
ids: Ids::new(id_gen),
|
|
messages: VecDeque::new(),
|
|
last_hover_pulse: None,
|
|
last_auto_show_pulse: None,
|
|
}
|
|
}
|
|
|
|
fn style(&self) -> Self::Style {}
|
|
|
|
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
|
|
let widget::UpdateArgs { state, ui, .. } = args;
|
|
|
|
// Tooltips
|
|
let item_tooltip = 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,
|
|
)
|
|
},
|
|
self.client,
|
|
self.imgs,
|
|
self.item_imgs,
|
|
self.pulse,
|
|
self.msm,
|
|
self.localized_strings,
|
|
)
|
|
.title_font_size(self.fonts.cyri.scale(20))
|
|
.parent(ui.window)
|
|
.desc_font_size(self.fonts.cyri.scale(12))
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.desc_text_color(TEXT_COLOR);
|
|
|
|
if !self.new_messages.is_empty() {
|
|
let pulse = self.pulse;
|
|
let oldest_merge_pulse = pulse - MAX_MERGE_TIME;
|
|
|
|
state.update(|s| {
|
|
s.messages.retain(|(message, t)| {
|
|
if *t >= oldest_merge_pulse {
|
|
if let Some(i) = self
|
|
.new_messages
|
|
.iter()
|
|
.position(|m| m.item.id() == message.item.id())
|
|
{
|
|
self.new_messages[i].amount += message.amount;
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
s.messages
|
|
.extend(self.new_messages.drain(..).map(|message| (message, pulse)));
|
|
while s.messages.len() > MAX_MESSAGES {
|
|
s.messages.pop_front();
|
|
}
|
|
});
|
|
ui.scroll_widget(state.ids.message_box, [0.0, f64::MAX]);
|
|
}
|
|
|
|
// check if it collides with other windows
|
|
if self.show.diary
|
|
|| self.show.map
|
|
|| self.show.open_windows != Windows::None
|
|
|| self.show.social
|
|
|| self.show.trade
|
|
{
|
|
if state.last_hover_pulse.is_some() || state.last_auto_show_pulse.is_some() {
|
|
state.update(|s| {
|
|
s.last_hover_pulse = None;
|
|
s.last_auto_show_pulse = None;
|
|
});
|
|
}
|
|
} else {
|
|
//check if hovered
|
|
if ui
|
|
.rect_of(state.ids.message_box)
|
|
.map(|r| r.pad_left(-6.0))
|
|
.map_or(false, |r| r.is_over(ui.global_input().current.mouse.xy))
|
|
{
|
|
state.update(|s| s.last_hover_pulse = Some(self.pulse));
|
|
}
|
|
|
|
if state.ids.message_icons.len() < state.messages.len() {
|
|
state.update(|s| {
|
|
s.ids
|
|
.message_icons
|
|
.resize(s.messages.len(), &mut ui.widget_id_generator())
|
|
});
|
|
}
|
|
if state.ids.message_icon_bgs.len() < state.messages.len() {
|
|
state.update(|s| {
|
|
s.ids
|
|
.message_icon_bgs
|
|
.resize(s.messages.len(), &mut ui.widget_id_generator())
|
|
});
|
|
}
|
|
if state.ids.message_icon_frames.len() < state.messages.len() {
|
|
state.update(|s| {
|
|
s.ids
|
|
.message_icon_frames
|
|
.resize(s.messages.len(), &mut ui.widget_id_generator())
|
|
});
|
|
}
|
|
if state.ids.message_texts.len() < state.messages.len() {
|
|
state.update(|s| {
|
|
s.ids
|
|
.message_texts
|
|
.resize(s.messages.len(), &mut ui.widget_id_generator())
|
|
});
|
|
}
|
|
if state.ids.message_text_shadows.len() < state.messages.len() {
|
|
state.update(|s| {
|
|
s.ids
|
|
.message_text_shadows
|
|
.resize(s.messages.len(), &mut ui.widget_id_generator())
|
|
});
|
|
}
|
|
|
|
let hover_age = state
|
|
.last_hover_pulse
|
|
.map_or(1.0, |t| (self.pulse - t) / HOVER_FADE_OUT_TIME);
|
|
let auto_show_age = state
|
|
.last_auto_show_pulse
|
|
.map_or(1.0, |t| (self.pulse - t) / AUTO_SHOW_FADE_OUT_TIME);
|
|
|
|
let show_all_age = hover_age.min(auto_show_age);
|
|
|
|
let messages_to_display = state
|
|
.messages
|
|
.iter()
|
|
.rev()
|
|
.map(|(message, t)| {
|
|
let age = (self.pulse - t) / MESSAGE_FADE_OUT_TIME;
|
|
(message, age)
|
|
})
|
|
.filter(|(_, age)| age.min(show_all_age) < 1.0)
|
|
.collect::<Vec<_>>();
|
|
|
|
let (mut list_messages, _) = List::flow_up(messages_to_display.len())
|
|
.w_h(BOX_WIDTH, BOX_HEIGHT)
|
|
.scroll_kids_vertically()
|
|
.bottom_left_with_margins_on(ui.window, 308.0, 20.0)
|
|
.set(state.ids.message_box, ui);
|
|
|
|
//only show scrollbar if it is being hovered and needed
|
|
if show_all_age < 1.0
|
|
&& ui
|
|
.widget_graph()
|
|
.widget(state.ids.message_box)
|
|
.and_then(|w| w.maybe_y_scroll_state)
|
|
.map_or(false, |s| s.scrollable_range_len > BOX_HEIGHT)
|
|
{
|
|
Scrollbar::y_axis(state.ids.message_box)
|
|
.thickness(5.0)
|
|
.rgba(0.33, 0.33, 0.33, 1.0 - show_all_age.powi(4))
|
|
.left_from(state.ids.message_box, 1.0)
|
|
.set(state.ids.scrollbar, ui);
|
|
}
|
|
|
|
while let Some(list_message) = list_messages.next(ui) {
|
|
let i = list_message.i;
|
|
|
|
let (message, age) = messages_to_display[i];
|
|
let LootMessage { item, amount } = message;
|
|
|
|
let alpha = 1.0 - age.min(show_all_age).powi(4);
|
|
|
|
let brightness = 1.0 / (age / 0.05 - 1.0).abs().clamp(0.01, 1.0);
|
|
|
|
let shade_color = |color: Color| {
|
|
let color::Hsla(hue, sat, lum, alp) = color.to_hsl();
|
|
color::hsla(hue, sat / brightness, lum * brightness.sqrt(), alp * alpha)
|
|
};
|
|
|
|
let quality_col_image = match item.quality {
|
|
Quality::Low => self.imgs.inv_slot_grey,
|
|
Quality::Common => self.imgs.inv_slot_common,
|
|
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,
|
|
};
|
|
let quality_col = get_quality_col(&**item);
|
|
|
|
Image::new(self.imgs.pixel)
|
|
.color(Some(shade_color(quality_col.alpha(0.7))))
|
|
.w_h(ICON_BG_SIZE, ICON_BG_SIZE)
|
|
.top_left_with_margins_on(list_message.widget_id, MESSAGE_VERTICAL_PADDING, 0.0)
|
|
.set(state.ids.message_icon_bgs[i], ui);
|
|
|
|
Image::new(quality_col_image)
|
|
.color(Some(shade_color(color::hsla(0.0, 0.0, 1.0, 1.0))))
|
|
.wh_of(state.ids.message_icon_bgs[i])
|
|
.middle_of(state.ids.message_icon_bgs[i])
|
|
.set(state.ids.message_icon_frames[i], ui);
|
|
|
|
Image::new(animate_by_pulse(
|
|
&self.item_imgs.img_ids_or_not_found_img((&**item).into()),
|
|
self.pulse,
|
|
))
|
|
.color(Some(shade_color(color::hsla(0.0, 0.0, 1.0, 1.0))))
|
|
.w_h(ICON_SIZE, ICON_SIZE)
|
|
.middle_of(state.ids.message_icon_bgs[i])
|
|
.with_item_tooltip(
|
|
self.item_tooltip_manager,
|
|
core::iter::once(&**item as &dyn ItemDesc),
|
|
&None,
|
|
&item_tooltip,
|
|
)
|
|
.set(state.ids.message_icons[i], ui);
|
|
|
|
let label = if *amount == 1 {
|
|
item.name.to_string()
|
|
} else {
|
|
format!("{}x {}", amount, item.name)
|
|
};
|
|
let label_font_size = 20;
|
|
|
|
Text::new(&label)
|
|
.top_left_with_margins_on(
|
|
list_message.widget_id,
|
|
MESSAGE_VERTICAL_PADDING + 1.0,
|
|
ICON_BG_SIZE + ICON_LABEL_SPACER,
|
|
)
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(label_font_size))
|
|
.color(shade_color(quality_col))
|
|
.graphics_for(state.ids.message_icons[i])
|
|
.and(|text| {
|
|
let text_width = match text.get_x_dimension(ui) {
|
|
Dimension::Absolute(x) => x,
|
|
_ => f64::MAX,
|
|
}
|
|
.min(BOX_WIDTH - (ICON_BG_SIZE + ICON_LABEL_SPACER));
|
|
text.w(text_width)
|
|
})
|
|
.set(state.ids.message_texts[i], ui);
|
|
Text::new(&label)
|
|
.depth(1.0)
|
|
.parent(list_message.widget_id)
|
|
.x_y_relative_to(state.ids.message_texts[i], -1.0, -1.0)
|
|
.wh_of(state.ids.message_texts[i])
|
|
.font_id(self.fonts.cyri.conrod_id)
|
|
.font_size(self.fonts.cyri.scale(label_font_size))
|
|
.color(shade_color(color::rgba(0.0, 0.0, 0.0, 1.0)))
|
|
.set(state.ids.message_text_shadows[i], ui);
|
|
|
|
let height = 2.0 * MESSAGE_VERTICAL_PADDING
|
|
+ ICON_BG_SIZE.max(
|
|
1.0 + ui
|
|
.rect_of(state.ids.message_texts[i])
|
|
.map_or(0.0, |r| r.h() + label_font_size as f64 / 3.0),
|
|
/* add to height since rect height does not account for lower parts of
|
|
* letters */
|
|
);
|
|
|
|
let rect = Rectangle::fill_with([BOX_WIDTH, height], color::TRANSPARENT);
|
|
|
|
list_message.set(rect, ui);
|
|
}
|
|
}
|
|
}
|
|
}
|