veloren_voxygen/hud/
overitem.rs

1use super::img_ids::Imgs;
2
3use crate::{
4    GlobalState,
5    game_input::GameInput,
6    hud::{
7        CollectFailedData, HudCollectFailedReason, HudLootOwner, IconHandler,
8        controller_icons::LayerIconIds,
9    },
10    ui::{Ingameable, fonts::Fonts},
11    window::LastInput,
12};
13use conrod_core::{
14    Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color,
15    widget::{self, RoundedRectangle, Text},
16    widget_ids,
17};
18use i18n::Localization;
19use std::borrow::Cow;
20
21pub const TEXT_COLOR: Color = Color::Rgba(0.61, 0.61, 0.89, 1.0);
22pub const NEGATIVE_TEXT_COLOR: Color = Color::Rgba(0.91, 0.15, 0.17, 1.0);
23pub const PICKUP_FAILED_FADE_OUT_TIME: f32 = 1.5;
24
25widget_ids! {
26    struct Ids {
27        // Name
28        name_bg,
29        name,
30        // Interaction hints
31        btn_bg,
32        btns[],
33        icns[], // controller icons
34        // Inventory full
35        inv_full_bg,
36        inv_full,
37    }
38}
39
40/// UI widget containing everything that goes over a item
41/// (Item, DistanceFromPlayer, Rarity, etc.)
42#[derive(WidgetCommon)]
43pub struct Overitem<'a> {
44    name: Cow<'a, str>,
45    quality: Color,
46    distance_from_player_sqr: f32,
47    fonts: &'a Fonts,
48    localized_strings: &'a Localization,
49    #[conrod(common_builder)]
50    common: widget::CommonBuilder,
51    properties: OveritemProperties,
52    pulse: f32,
53    // GameInput optional so we can just show stuff like "needs pickaxe"
54    interaction_options: Vec<(Option<GameInput>, String, Color)>,
55    imgs: &'a Imgs,
56    global_state: &'a GlobalState,
57}
58
59impl<'a> Overitem<'a> {
60    pub fn new(
61        name: Cow<'a, str>,
62        quality: Color,
63        distance_from_player_sqr: f32,
64        fonts: &'a Fonts,
65        localized_strings: &'a Localization,
66        properties: OveritemProperties,
67        pulse: f32,
68        interaction_options: Vec<(Option<GameInput>, String, Color)>,
69        imgs: &'a Imgs,
70        global_state: &'a GlobalState,
71    ) -> Self {
72        Self {
73            name,
74            quality,
75            distance_from_player_sqr,
76            fonts,
77            localized_strings,
78            common: widget::CommonBuilder::default(),
79            properties,
80            pulse,
81            interaction_options,
82            imgs,
83            global_state,
84        }
85    }
86}
87
88pub struct OveritemProperties {
89    pub active: bool,
90    pub pickup_failed_pulse: Option<CollectFailedData>,
91}
92
93pub struct State {
94    ids: Ids,
95}
96
97impl Ingameable for Overitem<'_> {
98    fn prim_count(&self) -> usize {
99        // Number of conrod primitives contained in the overitem display.
100        // TODO maybe this could be done automatically?
101
102        // + 2 Text for name
103        let base = 2;
104
105        // + 0 or 2 Rectangle and Text for button
106        let interaction_ids = match self.global_state.window.last_input() {
107            LastInput::KeyboardMouse => self
108                .global_state
109                .settings
110                .controls
111                .get_binding(GameInput::Interact)
112                .filter(|_| self.properties.active)
113                .map_or(0, |_| 2),
114            LastInput::Controller => {
115                // + 3 more than keyboard for icon ids (main icon, mod1, mod2)
116                self.global_state
117                    .settings
118                    .controller
119                    .get_game_button_binding(GameInput::Interact)
120                    .filter(|_| self.properties.active)
121                    .map_or(3, |_| 5)
122            },
123        };
124
125        // + 0 or 2 for pickup failed pulse
126        let pulse = if self.properties.pickup_failed_pulse.is_some() {
127            2
128        } else {
129            0
130        };
131
132        base + interaction_ids + pulse
133    }
134}
135
136impl Widget for Overitem<'_> {
137    type Event = ();
138    type State = State;
139    type Style = ();
140
141    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
142        State {
143            ids: Ids::new(id_gen),
144        }
145    }
146
147    fn style(&self) -> Self::Style {}
148
149    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
150        let widget::UpdateArgs { id, state, ui, .. } = args;
151
152        let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.8);
153
154        // Example:
155        //            MUSHROOM
156        //              ___
157        //             | E |
158        //              ———
159
160        // Scale at max distance is 10, and at min distance is 30. Disabled since the
161        // scaling ruins glyph caching, causing performance issues near lootbags
162        // let scale: f64 = ((1.5
163        //     - (self.distance_from_player_sqr /
164        //       common::consts::MAX_PICKUP_RANGE.powi(2)))
165        //     * 20.0)
166        //     .into();
167        let scale = 30.0;
168
169        let text_font_size = scale * 1.0;
170        let text_pos_y = scale * 1.2;
171
172        let btn_rect_size = scale * 0.8;
173        let btn_font_size = scale * 0.6;
174        let btn_rect_pos_y = 0.0;
175        let btn_text_pos_y = btn_rect_pos_y + ((btn_rect_size - btn_font_size) * 0.5);
176        let btn_radius = btn_rect_size / 5.0;
177
178        let inv_full_font_size = scale * 1.0;
179        let inv_full_pos_y = scale * 2.4;
180
181        // Item Name
182        Text::new(&self.name)
183            .font_id(self.fonts.cyri.conrod_id)
184            .font_size(text_font_size as u32)
185            .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
186            .x_y(-1.0, text_pos_y - 2.0)
187            .parent(id)
188            .depth(self.distance_from_player_sqr + 4.0)
189            .set(state.ids.name_bg, ui);
190        Text::new(&self.name)
191            .font_id(self.fonts.cyri.conrod_id)
192            .font_size(text_font_size as u32)
193            .color(self.quality)
194            .x_y(0.0, text_pos_y)
195            .depth(self.distance_from_player_sqr + 3.0)
196            .parent(id)
197            .set(state.ids.name, ui);
198
199        // Interaction hints
200        if !self.interaction_options.is_empty() && self.properties.active {
201            let mut max_w = btn_rect_size;
202            let mut max_h = 0.0;
203            let mut box_offset = 0.0;
204
205            match self.global_state.window.last_input() {
206                LastInput::KeyboardMouse => {
207                    let texts = self
208                        .interaction_options
209                        .iter()
210                        .filter_map(|(input, action, color)| {
211                            let binding = if let Some(input) = input {
212                                Some(self.global_state.settings.controls.get_binding(*input)?)
213                            } else {
214                                None
215                            };
216                            Some((binding, action, color))
217                        })
218                        .map(|(input, action, color)| {
219                            if let Some(input) = input {
220                                let input = input.display_string();
221                                (format!("{}  {action}", input.as_str()), color)
222                            } else {
223                                (action.to_string(), color)
224                            }
225                        })
226                        .collect::<Vec<_>>();
227                    if state.ids.btns.len() < texts.len() {
228                        state.update(|state| {
229                            state
230                                .ids
231                                .btns
232                                .resize(texts.len(), &mut ui.widget_id_generator());
233                        })
234                    }
235
236                    for (idx, (text, color)) in texts.iter().enumerate() {
237                        let hints_text = Text::new(text)
238                            .font_id(self.fonts.cyri.conrod_id)
239                            .font_size(btn_font_size as u32)
240                            .color(**color)
241                            .x_y(0.0, btn_text_pos_y + max_h)
242                            .depth(self.distance_from_player_sqr + 1.0)
243                            .parent(id);
244                        let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
245                        max_w = max_w.max(w);
246                        max_h += h;
247                        hints_text.set(state.ids.btns[idx], ui);
248                    }
249
250                    max_h = max_h.max(btn_rect_size);
251                },
252                LastInput::Controller => {
253                    // because in-line images are not easily supported, the controller icons are
254                    // manually rendered left of the text
255
256                    let controller_texts = self.interaction_options.iter().collect::<Vec<_>>(); // &Option<GameInput>, &String, &Color
257                    if state.ids.btns.len() < controller_texts.len() {
258                        state.update(|state| {
259                            state
260                                .ids
261                                .btns
262                                .resize(controller_texts.len(), &mut ui.widget_id_generator());
263                        })
264                    }
265                    let icns_size = controller_texts.len() * 3; // main icon + 2 modifier buttons
266                    if state.ids.icns.len() < icns_size {
267                        state.update(|state| {
268                            state
269                                .ids
270                                .icns
271                                .resize(icns_size, &mut ui.widget_id_generator());
272                        })
273                    }
274
275                    let icon_handler = IconHandler::new(self.global_state, self.imgs);
276                    let mut icons_w: u8 = 0;
277
278                    // render text here, call button next to it
279                    for (idx, (inputs, action, color)) in controller_texts.iter().enumerate() {
280                        // render text widget first
281                        let text_widget_id = state.ids.btns[idx];
282                        let hints_text = Text::new(action)
283                            .font_id(self.fonts.cyri.conrod_id)
284                            .font_size(btn_font_size as u32)
285                            .color(*color)
286                            .x_y(0.0, btn_text_pos_y + max_h)
287                            .depth(self.distance_from_player_sqr + 1.0)
288                            .parent(id);
289                        let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
290                        max_w = max_w.max(w);
291                        max_h += h;
292                        hints_text.set(text_widget_id, ui);
293
294                        // render controller icon left of the text
295                        let idx_icns = idx * 3;
296                        let icon_ids = LayerIconIds {
297                            main: state.ids.icns[idx_icns],
298                            modifier1: state.ids.icns[idx_icns + 1],
299                            modifier2: state.ids.icns[idx_icns + 2],
300                        };
301                        if let Some(input) = inputs {
302                            let count = icon_handler.set_controller_icons_left(
303                                *input,
304                                17.0,
305                                text_widget_id,
306                                &icon_ids,
307                                ui,
308                            );
309                            icons_w = icons_w.max(count);
310                        } else {
311                            // render transparant widgets to keep conrod from freaking out
312                            icon_handler.set_controller_icons_left_none(
313                                17.0,
314                                text_widget_id,
315                                &icon_ids,
316                                ui,
317                            );
318                        }
319                    }
320
321                    let icon_largest_width = icons_w as f64 * 21.0;
322                    box_offset = icon_largest_width / 2.0;
323                    max_w += icon_largest_width;
324                    max_h = max_h.max(btn_rect_size);
325                },
326            }
327
328            RoundedRectangle::fill_with(
329                [max_w + btn_radius * 2.0, max_h + btn_radius * 2.0],
330                btn_radius,
331                btn_color,
332            )
333            .x_y(0.0 - box_offset, btn_rect_pos_y)
334            .depth(self.distance_from_player_sqr + 2.0)
335            .parent(id)
336            .set(state.ids.btn_bg, ui);
337        }
338        if let Some(collect_failed_data) = self.properties.pickup_failed_pulse {
339            //should never exceed 1.0, but just in case
340            let age = ((self.pulse - collect_failed_data.pulse) / PICKUP_FAILED_FADE_OUT_TIME)
341                .clamp(0.0, 1.0);
342
343            let alpha = 1.0 - age.powi(4);
344            let brightness = 1.0 / (age / 0.07 - 1.0).abs().clamp(0.01, 1.0);
345            let shade_color = |color: Color| {
346                let color::Hsla(hue, sat, lum, alp) = color.to_hsl();
347                color::hsla(hue, sat / brightness, lum * brightness.sqrt(), alp * alpha)
348            };
349
350            let text = match collect_failed_data.reason {
351                HudCollectFailedReason::InventoryFull => {
352                    self.localized_strings.get_msg("hud-inventory_full")
353                },
354                HudCollectFailedReason::LootOwned { owner, expiry_secs } => {
355                    let owner_name = match owner {
356                        HudLootOwner::Name(name) => {
357                            Cow::Owned(self.localized_strings.get_content(&name))
358                        },
359                        HudLootOwner::Group => self.localized_strings.get_msg("hud-another_group"),
360                        HudLootOwner::Unknown => self.localized_strings.get_msg("hud-someone_else"),
361                    };
362                    self.localized_strings.get_msg_ctx(
363                        "hud-owned_by_for_secs",
364                        &i18n::fluent_args! {
365                            "name" => owner_name,
366                            "secs" => expiry_secs,
367                        },
368                    )
369                },
370            };
371
372            Text::new(&text)
373                .font_id(self.fonts.cyri.conrod_id)
374                .font_size(inv_full_font_size as u32)
375                .color(shade_color(Color::Rgba(0.0, 0.0, 0.0, 1.0)))
376                .x_y(-1.0, inv_full_pos_y - 2.0)
377                .parent(id)
378                .depth(self.distance_from_player_sqr + 6.0)
379                .set(state.ids.inv_full_bg, ui);
380
381            Text::new(&text)
382                .font_id(self.fonts.cyri.conrod_id)
383                .font_size(inv_full_font_size as u32)
384                .color(shade_color(Color::Rgba(1.0, 0.0, 0.0, 1.0)))
385                .x_y(0.0, inv_full_pos_y)
386                .parent(id)
387                .depth(self.distance_from_player_sqr + 5.0)
388                .set(state.ids.inv_full, ui);
389        }
390    }
391}