Skip to main content

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