veloren_voxygen/hud/
trade.rs

1use conrod_core::{
2    Color, Colorable, Labelable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, color,
3    position::Relative,
4    widget::{self, Button, Image, Rectangle, State as ConrodState, Text, TextEdit},
5    widget_ids,
6};
7use specs::{Entity as EcsEntity, WorldExt};
8use vek::*;
9
10use client::Client;
11use common::{
12    comp::{
13        Inventory, Stats,
14        inventory::item::{ItemDesc, ItemI18n, MaterialStatManifest, Quality},
15    },
16    recipe::RecipeBookManifest,
17    trade::{PendingTrade, SitePrices, TradeAction, TradePhase},
18};
19use common_net::sync::WorldSyncExt;
20use i18n::Localization;
21
22use crate::{
23    hud::{
24        Event as HudEvent, PromptDialogSettings,
25        bag::{BackgroundIds, InventoryScroller},
26    },
27    ui::{
28        ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable, Tooltip, TooltipManager,
29        Tooltipable,
30        fonts::Fonts,
31        slot::{ContentSize, SlotMaker},
32    },
33};
34
35use super::{
36    Hud, HudInfo, Show, TEXT_COLOR, TEXT_GRAY_COLOR, TradeAmountInput, UI_HIGHLIGHT_0, UI_MAIN,
37    img_ids::{Imgs, ImgsRot},
38    item_imgs::ItemImgs,
39    slots::{SlotKind, SlotManager, TradeSlot},
40    util,
41};
42use std::borrow::Cow;
43
44#[allow(clippy::large_enum_variant)]
45pub enum TradeEvent {
46    TradeAction(TradeAction),
47    SetDetailsMode(bool),
48    HudUpdate(HudUpdate),
49    ShowPrompt(PromptDialogSettings),
50}
51
52#[derive(Debug)]
53pub enum HudUpdate {
54    Focus(widget::Id),
55    Submit,
56}
57
58pub struct State {
59    ids: Ids,
60    bg_ids: BackgroundIds,
61}
62
63widget_ids! {
64    pub struct Ids {
65        trade_close,
66        bg,
67        bg_frame,
68        trade_title_bg,
69        trade_title,
70        inv_alignment[],
71        inv_slots[],
72        inv_textslots[],
73        offer_headers[],
74        accept_indicators[],
75        phase_indicator,
76        accept_button,
77        decline_button,
78        inventory_scroller,
79        amount_bg,
80        amount_notice,
81        amount_open_label,
82        amount_open_btn,
83        amount_open_ovlay,
84        amount_input,
85        amount_btn,
86        trade_details_btn,
87    }
88}
89
90#[derive(WidgetCommon)]
91pub struct Trade<'a> {
92    client: &'a Client,
93    info: &'a HudInfo<'a>,
94    imgs: &'a Imgs,
95    item_imgs: &'a ItemImgs,
96    fonts: &'a Fonts,
97    rot_imgs: &'a ImgsRot,
98    tooltip_manager: &'a mut TooltipManager,
99    item_tooltip_manager: &'a mut ItemTooltipManager,
100    #[conrod(common_builder)]
101    common: widget::CommonBuilder,
102    slot_manager: &'a mut SlotManager,
103    localized_strings: &'a Localization,
104    item_i18n: &'a ItemI18n,
105    msm: &'a MaterialStatManifest,
106    rbm: &'a RecipeBookManifest,
107    pulse: f32,
108    show: &'a mut Show,
109    needs_thirdconfirm: bool,
110}
111
112impl<'a> Trade<'a> {
113    pub fn new(
114        client: &'a Client,
115        info: &'a HudInfo,
116        imgs: &'a Imgs,
117        item_imgs: &'a ItemImgs,
118        fonts: &'a Fonts,
119        rot_imgs: &'a ImgsRot,
120        tooltip_manager: &'a mut TooltipManager,
121        item_tooltip_manager: &'a mut ItemTooltipManager,
122        slot_manager: &'a mut SlotManager,
123        localized_strings: &'a Localization,
124        item_i18n: &'a ItemI18n,
125        msm: &'a MaterialStatManifest,
126        rbm: &'a RecipeBookManifest,
127        pulse: f32,
128        show: &'a mut Show,
129    ) -> Self {
130        Self {
131            client,
132            info,
133            imgs,
134            item_imgs,
135            fonts,
136            rot_imgs,
137            tooltip_manager,
138            item_tooltip_manager,
139            common: widget::CommonBuilder::default(),
140            slot_manager,
141            localized_strings,
142            item_i18n,
143            msm,
144            rbm,
145            pulse,
146            show,
147            needs_thirdconfirm: false,
148        }
149    }
150}
151
152const MAX_TRADE_SLOTS: usize = 16;
153
154impl<'a> Trade<'a> {
155    fn background(&mut self, state: &mut ConrodState<'_, State>, ui: &mut UiCell<'_>) {
156        Image::new(self.imgs.inv_middle_bg_bag)
157            .w_h(424.0, 482.0)
158            .color(Some(UI_MAIN))
159            .mid_bottom_with_margin_on(ui.window, 295.0)
160            .set(state.ids.bg, ui);
161        Image::new(self.imgs.inv_middle_frame)
162            .w_h(424.0, 482.0)
163            .middle_of(state.ids.bg)
164            .color(Some(UI_HIGHLIGHT_0))
165            .set(state.ids.bg_frame, ui);
166    }
167
168    fn title(&mut self, state: &mut ConrodState<'_, State>, ui: &mut UiCell<'_>) {
169        Text::new(&self.localized_strings.get_msg("hud-trade-trade_window"))
170            .mid_top_with_margin_on(state.ids.bg_frame, 9.0)
171            .font_id(self.fonts.cyri.conrod_id)
172            .font_size(self.fonts.cyri.scale(20))
173            .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
174            .set(state.ids.trade_title_bg, ui);
175        Text::new(&self.localized_strings.get_msg("hud-trade-trade_window"))
176            .top_left_with_margins_on(state.ids.trade_title_bg, 2.0, 2.0)
177            .font_id(self.fonts.cyri.conrod_id)
178            .font_size(self.fonts.cyri.scale(20))
179            .color(TEXT_COLOR)
180            .set(state.ids.trade_title, ui);
181    }
182
183    fn phase_indicator(
184        &mut self,
185        state: &mut ConrodState<'_, State>,
186        ui: &mut UiCell<'_>,
187        trade: &'a PendingTrade,
188    ) {
189        let phase_text = match trade.phase() {
190            TradePhase::Mutate => self
191                .localized_strings
192                .get_msg("hud-trade-phase1_description"),
193            TradePhase::Review => self
194                .localized_strings
195                .get_msg("hud-trade-phase2_description"),
196            TradePhase::Complete => self
197                .localized_strings
198                .get_msg("hud-trade-phase3_description"),
199        };
200
201        Text::new(&phase_text)
202            .mid_top_with_margin_on(state.ids.bg, 70.0)
203            .font_id(self.fonts.cyri.conrod_id)
204            .font_size(self.fonts.cyri.scale(20))
205            .color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
206            .set(state.ids.phase_indicator, ui);
207    }
208
209    fn item_pane(
210        &mut self,
211        state: &mut ConrodState<'_, State>,
212        ui: &mut UiCell<'_>,
213        trade: &'a PendingTrade,
214        prices: &'a Option<SitePrices>,
215        ours: bool,
216    ) -> Option<TradeEvent> {
217        let mut event = None;
218        let inventories = self.client.inventories();
219        let check_if_us = |who: usize| -> Option<_> {
220            let uid = trade.parties[who];
221            let entity = self.client.state().ecs().entity_from_uid(uid)?;
222            let is_ours = entity == self.client.entity();
223            Some(((who, uid, entity), is_ours))
224        };
225        let (who, uid, entity) = match check_if_us(0)? {
226            (x, is_ours) if ours == is_ours => x,
227            _ => check_if_us(1)?.0,
228        };
229
230        // TODO: update in accordance with https://gitlab.com/veloren/veloren/-/issues/960
231        let inventory = inventories.get(entity)?;
232        // Get our inventory to use it in item tooltips
233        // Our inventory is needed to know what recipes are known
234        let our_inventory = if entity == self.client.entity() {
235            inventory
236        } else {
237            let uid = trade.parties[if who == 0 { 1 } else { 0 }];
238            let entity = self.client.state().ecs().entity_from_uid(uid)?;
239            inventories.get(entity)?
240        };
241
242        // Alignment for Grid
243        let mut alignment = Rectangle::fill_with([200.0, 180.0], color::TRANSPARENT);
244        if !ours {
245            alignment = alignment.top_left_with_margins_on(state.ids.bg, 180.0, 32.5);
246        } else {
247            alignment = alignment.right_from(state.ids.inv_alignment[1 - who], 0.0);
248        }
249        alignment
250            .scroll_kids_vertically()
251            .set(state.ids.inv_alignment[who], ui);
252
253        // Retrieve player's name and gender from player list or default to Player {idx}
254        let name = self
255            .client
256            .player_list()
257            .get(&uid)
258            .map(|info| info.player_alias.clone())
259            .or_else(|| {
260                self.client
261                    .state()
262                    .read_storage::<Stats>()
263                    .get(entity)
264                    .map(|e| self.localized_strings.get_content(&e.name))
265            })
266            .unwrap_or_else(|| {
267                // Using this method because player_info isn't accessible here
268                let ecs = self.client.state().ecs();
269                let bodies = ecs.read_storage::<common::comp::Body>();
270                let body = bodies.get(entity);
271                let gender_enum = body.and_then(|b| b.humanoid_gender());
272
273                // Determine gender string
274                let gender_str = match gender_enum {
275                    Some(common::comp::body::Gender::Feminine) => "she",
276                    Some(common::comp::body::Gender::Masculine) => "he",
277                    _ => "other", // Fallback
278                };
279
280                self.localized_strings
281                    .get_msg_ctx("hud-trade-player_who", &i18n::fluent_args! {
282                        "player_who" => who,
283                        "player_gender" => gender_str
284                    })
285                    .into_owned()
286            });
287
288        let offer_header = if ours {
289            self.localized_strings.get_msg("hud-trade-your_offer")
290        } else {
291            self.localized_strings.get_msg("hud-trade-their_offer")
292        };
293
294        Text::new(&offer_header)
295            .up_from(state.ids.inv_alignment[who], 20.0)
296            .font_id(self.fonts.cyri.conrod_id)
297            .font_size(self.fonts.cyri.scale(20))
298            .color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
299            .set(state.ids.offer_headers[who], ui);
300
301        let has_accepted = trade.accept_flags[who];
302        let accept_indicator =
303            self.localized_strings
304                .get_msg_ctx("hud-trade-has_accepted", &i18n::fluent_args! {
305                    "playername" => &name,
306                });
307        Text::new(&accept_indicator)
308            .down_from(state.ids.inv_alignment[who], 50.0)
309            .font_id(self.fonts.cyri.conrod_id)
310            .font_size(self.fonts.cyri.scale(20))
311            .color(Color::Rgba(
312                1.0,
313                1.0,
314                1.0,
315                if has_accepted { 1.0 } else { 0.0 },
316            ))
317            .set(state.ids.accept_indicators[who], ui);
318
319        let mut invslots: Vec<_> = trade.offers[who].iter().map(|(k, v)| (*k, *v)).collect();
320        invslots.sort();
321        let tradeslots: Vec<_> = invslots
322            .into_iter()
323            .enumerate()
324            .map(|(index, (k, quantity))| TradeSlot {
325                index,
326                quantity,
327                invslot: Some(k),
328                ours,
329                entity,
330            })
331            .collect();
332
333        if matches!(trade.phase(), TradePhase::Mutate) {
334            event = self
335                .phase1_itemwidget(
336                    state,
337                    ui,
338                    inventory,
339                    our_inventory,
340                    who,
341                    ours,
342                    entity,
343                    name,
344                    prices,
345                    &tradeslots,
346                )
347                .or(event);
348        } else {
349            self.phase2_itemwidget(state, ui, inventory, who, ours, entity, &tradeslots);
350        }
351
352        event
353    }
354
355    fn phase1_itemwidget(
356        &mut self,
357        state: &mut ConrodState<'_, State>,
358        ui: &mut UiCell<'_>,
359        inventory: &Inventory,
360        // Used for item tooltip
361        our_inventory: &Inventory,
362        who: usize,
363        ours: bool,
364        entity: EcsEntity,
365        name: String,
366        prices: &'a Option<SitePrices>,
367        tradeslots: &[TradeSlot],
368    ) -> Option<TradeEvent> {
369        let mut event = None;
370        // Tooltips
371        let item_tooltip = ItemTooltip::new(
372            {
373                // Edge images [t, b, r, l]
374                // Corner images [tr, tl, br, bl]
375                let edge = &self.rot_imgs.tt_side;
376                let corner = &self.rot_imgs.tt_corner;
377                ImageFrame::new(
378                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
379                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
380                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
381                    5.0,
382                )
383            },
384            self.client,
385            self.info,
386            self.imgs,
387            self.item_imgs,
388            self.pulse,
389            self.msm,
390            self.rbm,
391            Some(our_inventory),
392            self.localized_strings,
393            self.item_i18n,
394        )
395        .title_font_size(self.fonts.cyri.scale(20))
396        .parent(ui.window)
397        .desc_font_size(self.fonts.cyri.scale(12))
398        .font_id(self.fonts.cyri.conrod_id)
399        .desc_text_color(TEXT_COLOR);
400
401        if !ours {
402            InventoryScroller::new(
403                self.client,
404                self.imgs,
405                self.item_imgs,
406                self.fonts,
407                self.item_tooltip_manager,
408                self.slot_manager,
409                self.pulse,
410                self.localized_strings,
411                self.item_i18n,
412                false,
413                true,
414                false,
415                &item_tooltip,
416                name,
417                entity,
418                false,
419                inventory,
420                &state.bg_ids,
421                false,
422                self.show.trade_details,
423            )
424            .set(state.ids.inventory_scroller, ui);
425
426            let bag_tooltip = Tooltip::new({
427                // Edge images [t, b, r, l]
428                // Corner images [tr, tl, br, bl]
429                let edge = &self.rot_imgs.tt_side;
430                let corner = &self.rot_imgs.tt_corner;
431                ImageFrame::new(
432                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
433                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
434                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
435                    5.0,
436                )
437            })
438            .title_font_size(self.fonts.cyri.scale(15))
439            .parent(ui.window)
440            .desc_font_size(self.fonts.cyri.scale(12))
441            .font_id(self.fonts.cyri.conrod_id)
442            .desc_text_color(TEXT_COLOR);
443
444            let buttons_top = 53.0;
445            let (txt, btn, hover, press) = if self.show.trade_details {
446                (
447                    "Grid mode",
448                    self.imgs.grid_btn,
449                    self.imgs.grid_btn_hover,
450                    self.imgs.grid_btn_press,
451                )
452            } else {
453                (
454                    "List mode",
455                    self.imgs.list_btn,
456                    self.imgs.list_btn_hover,
457                    self.imgs.list_btn_press,
458                )
459            };
460            let details_btn = Button::image(btn)
461                .w_h(32.0, 17.0)
462                .hover_image(hover)
463                .press_image(press);
464            if details_btn
465                .mid_top_with_margin_on(state.bg_ids.bg_frame, buttons_top)
466                .with_tooltip(self.tooltip_manager, txt, "", &bag_tooltip, TEXT_COLOR)
467                .set(state.ids.trade_details_btn, ui)
468                .was_clicked()
469            {
470                event = Some(TradeEvent::SetDetailsMode(!self.show.trade_details));
471            }
472        }
473
474        let mut slot_maker = SlotMaker {
475            empty_slot: self.imgs.inv_slot,
476            filled_slot: self.imgs.inv_slot,
477            selected_slot: self.imgs.inv_slot_sel,
478            background_color: Some(UI_MAIN),
479            content_size: ContentSize {
480                width_height_ratio: 1.0,
481                max_fraction: 0.75,
482            },
483            selected_content_scale: 1.067,
484            amount_font: self.fonts.cyri.conrod_id,
485            amount_margins: Vec2::new(-4.0, 0.0),
486            amount_font_size: self.fonts.cyri.scale(12),
487            amount_text_color: TEXT_COLOR,
488            content_source: inventory,
489            image_source: self.item_imgs,
490            slot_manager: Some(self.slot_manager),
491            pulse: self.pulse,
492        };
493
494        if state.ids.inv_slots.len() < 2 * MAX_TRADE_SLOTS {
495            state.update(|s| {
496                s.ids
497                    .inv_slots
498                    .resize(2 * MAX_TRADE_SLOTS, &mut ui.widget_id_generator());
499            });
500        }
501
502        for i in 0..MAX_TRADE_SLOTS {
503            let x = i % 4;
504            let y = i / 4;
505
506            let slot = tradeslots.get(i).cloned().unwrap_or(TradeSlot {
507                index: i,
508                quantity: 0,
509                invslot: None,
510                ours,
511                entity,
512            });
513            // Slot
514            let slot_widget = slot_maker
515                .fabricate(slot, [40.0; 2])
516                .top_left_with_margins_on(
517                    state.ids.inv_alignment[who],
518                    0.0 + y as f64 * (40.0),
519                    0.0 + x as f64 * (40.0),
520                );
521            let slot_id = state.ids.inv_slots[i + who * MAX_TRADE_SLOTS];
522            if let Some(Some(item)) = slot.invslot.and_then(|slotid| inventory.slot(slotid)) {
523                let quality_col_img = match item.quality() {
524                    Quality::Low => self.imgs.inv_slot_grey,
525                    Quality::Common => self.imgs.inv_slot_common,
526                    Quality::Moderate => self.imgs.inv_slot_green,
527                    Quality::High => self.imgs.inv_slot_blue,
528                    Quality::Epic => self.imgs.inv_slot_purple,
529                    Quality::Legendary => self.imgs.inv_slot_gold,
530                    Quality::Artifact => self.imgs.inv_slot_orange,
531                    _ => self.imgs.inv_slot_red,
532                };
533
534                slot_widget
535                    .filled_slot(quality_col_img)
536                    .with_item_tooltip(
537                        self.item_tooltip_manager,
538                        core::iter::once(item as &dyn ItemDesc),
539                        prices,
540                        &item_tooltip,
541                    )
542                    .set(slot_id, ui);
543            } else {
544                slot_widget.set(slot_id, ui);
545            }
546        }
547        event
548    }
549
550    fn phase2_itemwidget(
551        &mut self,
552        state: &mut ConrodState<'_, State>,
553        ui: &mut UiCell<'_>,
554        inventory: &Inventory,
555        who: usize,
556        ours: bool,
557        entity: EcsEntity,
558        tradeslots: &[TradeSlot],
559    ) {
560        if state.ids.inv_textslots.len() < 2 * MAX_TRADE_SLOTS {
561            state.update(|s| {
562                s.ids
563                    .inv_textslots
564                    .resize(2 * MAX_TRADE_SLOTS, &mut ui.widget_id_generator());
565            });
566        }
567        let max_width = 170.0;
568        let mut total_text_height = 0.0;
569        let mut total_quantity = 0;
570        for i in 0..MAX_TRADE_SLOTS {
571            let slot = tradeslots.get(i).cloned().unwrap_or(TradeSlot {
572                index: i,
573                quantity: 0,
574                invslot: None,
575                ours,
576                entity,
577            });
578            total_quantity += slot.quantity;
579            let itemname = slot
580                .invslot
581                .and_then(|i| inventory.get(i))
582                .map(|i| {
583                    let (name, _) = util::item_text(&i, self.localized_strings, self.item_i18n);
584
585                    Cow::Owned(name)
586                })
587                .unwrap_or(Cow::Borrowed(""));
588            let is_present = slot.quantity > 0 && slot.invslot.is_some();
589            Text::new(&format!("{}x {}", slot.quantity, &itemname))
590                .top_left_with_margins_on(
591                    state.ids.inv_alignment[who],
592                    15.0 + i as f64 * 20.0 + total_text_height,
593                    0.0,
594                )
595                .font_id(self.fonts.cyri.conrod_id)
596                .font_size(self.fonts.cyri.scale(20))
597                .wrap_by_word()
598                .w(max_width)
599                .color(Color::Rgba(
600                    1.0,
601                    1.0,
602                    1.0,
603                    if is_present { 1.0 } else { 0.0 },
604                ))
605                .set(state.ids.inv_textslots[i + who * MAX_TRADE_SLOTS], ui);
606            let label_height = match ui
607                .widget_graph()
608                .widget(state.ids.inv_textslots[i + who * MAX_TRADE_SLOTS])
609                .map(|widget| widget.rect)
610            {
611                Some(label_rect) => label_rect.h(),
612                None => 10.0,
613            };
614            total_text_height += label_height;
615        }
616        if total_quantity == 0 {
617            Text::new("Nothing!")
618                .top_left_with_margins_on(state.ids.inv_alignment[who], 10.0, 0.0)
619                .font_id(self.fonts.cyri.conrod_id)
620                .font_size(self.fonts.cyri.scale(20))
621                .color(Color::Rgba(
622                    1.0,
623                    0.25 + 0.25 * (4.0 * self.pulse).sin(),
624                    0.0,
625                    1.0,
626                ))
627                .set(state.ids.inv_textslots[who * MAX_TRADE_SLOTS], ui);
628
629            if !ours {
630                self.needs_thirdconfirm = true;
631            }
632        }
633    }
634
635    fn accept_decline_buttons(
636        &mut self,
637        state: &mut ConrodState<'_, State>,
638        ui: &mut UiCell<'_>,
639        trade: &'a PendingTrade,
640    ) -> Option<TradeEvent> {
641        let mut event = None;
642        let (hover_img, press_img, accept_button_luminance) = if trade.is_empty_trade() {
643            //Darken the accept button if the trade is empty.
644            (
645                self.imgs.button,
646                self.imgs.button,
647                Color::Rgba(0.6, 0.6, 0.6, 1.0),
648            )
649        } else {
650            (
651                self.imgs.button_hover,
652                self.imgs.button_press,
653                Color::Rgba(1.0, 1.0, 1.0, 1.0),
654            )
655        };
656        if Button::image(self.imgs.button)
657            .w_h(31.0 * 5.0, 12.0 * 2.0)
658            .hover_image(hover_img)
659            .press_image(press_img)
660            .image_color(accept_button_luminance)
661            .bottom_left_with_margins_on(state.ids.bg, 90.0, 47.0)
662            .label(&self.localized_strings.get_msg("hud-trade-accept"))
663            .label_font_size(self.fonts.cyri.scale(14))
664            .label_color(TEXT_COLOR)
665            .label_font_id(self.fonts.cyri.conrod_id)
666            .label_y(Relative::Scalar(2.0))
667            .set(state.ids.accept_button, ui)
668            .was_clicked()
669        {
670            if matches!(trade.phase, TradePhase::Review) && self.needs_thirdconfirm {
671                event = Some(TradeEvent::ShowPrompt(PromptDialogSettings::new(
672                    self.localized_strings
673                        .get_msg("hud-confirm-trade-for-nothing")
674                        .to_string(),
675                    HudEvent::TradeAction(TradeAction::Accept(trade.phase())),
676                    None,
677                )));
678            } else {
679                event = Some(TradeEvent::TradeAction(TradeAction::Accept(trade.phase())));
680            }
681        }
682
683        if Button::image(self.imgs.button)
684            .w_h(31.0 * 5.0, 12.0 * 2.0)
685            .hover_image(self.imgs.button_hover)
686            .press_image(self.imgs.button_press)
687            .right_from(state.ids.accept_button, 20.0)
688            .label(&self.localized_strings.get_msg("hud-trade-decline"))
689            .label_font_size(self.fonts.cyri.scale(14))
690            .label_color(TEXT_COLOR)
691            .label_font_id(self.fonts.cyri.conrod_id)
692            .label_y(Relative::Scalar(2.0))
693            .set(state.ids.decline_button, ui)
694            .was_clicked()
695        {
696            event = Some(TradeEvent::TradeAction(TradeAction::Decline));
697        }
698        event
699    }
700
701    fn input_item_amount(
702        &mut self,
703        state: &mut ConrodState<'_, State>,
704        ui: &mut UiCell<'_>,
705        trade: &'a PendingTrade,
706    ) -> Option<TradeEvent> {
707        let mut event = None;
708        let selected = self.slot_manager.selected().and_then(|s| match s {
709            SlotKind::Trade(t_s) => t_s.invslot.and_then(|slot| {
710                let who: usize = trade.offers[0].get(&slot).and(Some(0)).unwrap_or(1);
711                self.client
712                    .inventories()
713                    .get(t_s.entity)?
714                    .get(slot)
715                    .map(|item| (t_s.ours, slot, item.amount(), who))
716            }),
717            _ => None,
718        });
719        Rectangle::fill([132.0, 20.0])
720            .bottom_right_with_margins_on(state.ids.bg_frame, 16.0, 32.0)
721            .hsla(
722                0.0,
723                0.0,
724                0.0,
725                if self.show.trade_amount_input_key.is_some() {
726                    0.75
727                } else {
728                    0.35
729                },
730            )
731            .set(state.ids.amount_bg, ui);
732        if let Some((ours, slot, inv, who)) = selected {
733            self.show.trade_amount_input_key = None;
734            // Text for the amount of items offered.
735            let input = trade.offers[who]
736                .get(&slot)
737                .map(|u| format!("{}", u))
738                .unwrap_or_else(String::new);
739            Text::new(&input)
740                .top_left_with_margins_on(state.ids.amount_bg, 0.0, 22.0)
741                .font_id(self.fonts.cyri.conrod_id)
742                .font_size(self.fonts.cyri.scale(14))
743                .color(TEXT_COLOR.alpha(0.7))
744                .set(state.ids.amount_open_label, ui);
745            if Button::image(self.imgs.edit_btn)
746                .hover_image(self.imgs.edit_btn_hover)
747                .press_image(self.imgs.edit_btn_press)
748                .mid_left_with_margin_on(state.ids.amount_bg, 2.0)
749                .w_h(16.0, 16.0)
750                .set(state.ids.amount_open_btn, ui)
751                .was_clicked()
752            {
753                event = Some(HudUpdate::Focus(state.ids.amount_input));
754                self.slot_manager.idle();
755                self.show.trade_amount_input_key =
756                    Some(TradeAmountInput::new(slot, input, inv, ours, who));
757            }
758            Rectangle::fill_with([132.0, 20.0], color::TRANSPARENT)
759                .top_left_of(state.ids.amount_bg)
760                .graphics_for(state.ids.amount_open_btn)
761                .set(state.ids.amount_open_ovlay, ui);
762        } else if let Some(key) = &mut self.show.trade_amount_input_key {
763            if !Hud::is_captured::<TextEdit>(ui) && key.input_painted {
764                // If the text edit is not captured submit the amount.
765                event = Some(HudUpdate::Submit);
766            }
767
768            if Button::image(self.imgs.close_btn)
769                .hover_image(self.imgs.close_btn_hover)
770                .press_image(self.imgs.close_btn_press)
771                .mid_left_with_margin_on(state.ids.amount_bg, 2.0)
772                .w_h(16.0, 16.0)
773                .set(state.ids.amount_btn, ui)
774                .was_clicked()
775            {
776                event = Some(HudUpdate::Submit);
777            }
778            // Input for making TradeAction requests
779            key.input_painted = true;
780            let text_color = key.err.as_ref().and(Some(color::RED)).unwrap_or(TEXT_COLOR);
781            if let Some(new_input) = TextEdit::new(&key.input)
782                .mid_left_with_margin_on(state.ids.amount_bg, 22.0)
783                .w_h(138.0, 20.0)
784                .font_id(self.fonts.cyri.conrod_id)
785                .font_size(self.fonts.cyri.scale(14))
786                .color(text_color)
787                .set(state.ids.amount_input, ui)
788            {
789                if new_input != key.input {
790                    new_input.trim().clone_into(&mut key.input);
791                    if !key.input.is_empty() {
792                        // trade amount can change with (shift||ctrl)-click
793                        let amount = *trade.offers[key.who].get(&key.slot).unwrap_or(&0);
794                        match key.input.parse::<i32>() {
795                            Ok(new_amount) => {
796                                key.input = format!("{}", new_amount);
797                                if new_amount > -1 && new_amount <= key.inv as i32 {
798                                    key.err = None;
799                                    let delta = new_amount - amount as i32;
800                                    key.submit_action =
801                                        TradeAction::item(key.slot, delta, key.ours);
802                                } else {
803                                    key.err = Some("out of range".to_owned());
804                                    key.submit_action = None;
805                                }
806                            },
807                            Err(_) => {
808                                key.err = Some("bad quantity".to_owned());
809                                key.submit_action = None;
810                            },
811                        }
812                    } else {
813                        key.submit_action = None;
814                    }
815                }
816            }
817        } else {
818            // placeholder text when no trade slot is selected
819            Text::new(&self.localized_strings.get_msg("hud-trade-amount_input"))
820                .middle_of(state.ids.amount_bg)
821                .font_id(self.fonts.cyri.conrod_id)
822                .font_size(self.fonts.cyri.scale(14))
823                .color(TEXT_GRAY_COLOR.alpha(0.25))
824                .set(state.ids.amount_notice, ui);
825        }
826        event.map(TradeEvent::HudUpdate)
827    }
828
829    fn close_button(
830        &mut self,
831        state: &mut ConrodState<'_, State>,
832        ui: &mut UiCell<'_>,
833    ) -> Option<TradeEvent> {
834        if Button::image(self.imgs.close_btn)
835            .w_h(24.0, 25.0)
836            .hover_image(self.imgs.close_btn_hover)
837            .press_image(self.imgs.close_btn_press)
838            .top_right_with_margins_on(state.ids.bg, 0.0, 0.0)
839            .set(state.ids.trade_close, ui)
840            .was_clicked()
841        {
842            Some(TradeEvent::TradeAction(TradeAction::Decline))
843        } else {
844            None
845        }
846    }
847}
848
849impl Widget for Trade<'_> {
850    type Event = Option<TradeEvent>;
851    type State = State;
852    type Style = ();
853
854    fn init_state(&self, mut id_gen: widget::id::Generator) -> Self::State {
855        State {
856            bg_ids: BackgroundIds {
857                bg: id_gen.next(),
858                bg_frame: id_gen.next(),
859            },
860            ids: Ids::new(id_gen),
861        }
862    }
863
864    fn style(&self) -> Self::Style {}
865
866    fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
867        common_base::prof_span!("Trade::update");
868        let widget::UpdateArgs { state, ui, .. } = args;
869
870        let mut event = None;
871        let (trade, prices) = match self.client.pending_trade() {
872            Some((_, trade, prices)) => (trade, prices),
873            None => return Some(TradeEvent::TradeAction(TradeAction::Decline)),
874        };
875
876        if state.ids.inv_alignment.len() < 2 {
877            state.update(|s| {
878                s.ids.inv_alignment.resize(2, &mut ui.widget_id_generator());
879            });
880        }
881        if state.ids.offer_headers.len() < 2 {
882            state.update(|s| {
883                s.ids.offer_headers.resize(2, &mut ui.widget_id_generator());
884            });
885        }
886        if state.ids.accept_indicators.len() < 2 {
887            state.update(|s| {
888                s.ids
889                    .accept_indicators
890                    .resize(2, &mut ui.widget_id_generator());
891            });
892        }
893
894        self.background(state, ui);
895        self.title(state, ui);
896        self.phase_indicator(state, ui, trade);
897
898        event = self.item_pane(state, ui, trade, prices, false).or(event);
899        event = self.item_pane(state, ui, trade, prices, true).or(event);
900        event = self.accept_decline_buttons(state, ui, trade).or(event);
901        event = self.close_button(state, ui).or(event);
902        self.input_item_amount(state, ui, trade).or(event)
903    }
904}