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 let inventory = inventories.get(entity)?;
238 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 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 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 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 let gender_str = match gender_enum {
281 Some(common::comp::body::Gender::Feminine) => "she",
282 Some(common::comp::body::Gender::Masculine) => "he",
283 _ => "other", };
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 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 let item_tooltip = ItemTooltip::new(
378 {
379 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 self.localized_strings,
418 self.item_i18n,
419 false,
420 true,
421 false,
422 &item_tooltip,
423 name,
424 entity,
425 false,
426 inventory,
427 &state.bg_ids,
428 false,
429 self.show.trade_details,
430 )
431 .set(state.ids.inventory_scroller, ui)
432 {
433 if self
434 .global_state
435 .settings
436 .interface
437 .toggle_draggable_windows
438 {
439 let InventoryScrollerEvent::Drag(pos) = event;
440 events.push(TradeEvent::MoveBag(pos));
441 }
442 }
443
444 let bag_tooltip = Tooltip::new({
445 let edge = &self.rot_imgs.tt_side;
448 let corner = &self.rot_imgs.tt_corner;
449 ImageFrame::new(
450 [edge.cw180, edge.none, edge.cw270, edge.cw90],
451 [corner.none, corner.cw270, corner.cw90, corner.cw180],
452 Color::Rgba(0.08, 0.07, 0.04, 1.0),
453 5.0,
454 )
455 })
456 .title_font_size(self.fonts.cyri.scale(15))
457 .parent(ui.window)
458 .desc_font_size(self.fonts.cyri.scale(12))
459 .font_id(self.fonts.cyri.conrod_id)
460 .desc_text_color(TEXT_COLOR);
461
462 let buttons_top = 53.0;
463 let (txt, btn, hover, press) = if self.show.trade_details {
464 (
465 "Grid mode",
466 self.imgs.grid_btn,
467 self.imgs.grid_btn_hover,
468 self.imgs.grid_btn_press,
469 )
470 } else {
471 (
472 "List mode",
473 self.imgs.list_btn,
474 self.imgs.list_btn_hover,
475 self.imgs.list_btn_press,
476 )
477 };
478 let details_btn = Button::image(btn)
479 .w_h(32.0, 17.0)
480 .hover_image(hover)
481 .press_image(press);
482 if details_btn
483 .mid_top_with_margin_on(state.bg_ids.bg_frame, buttons_top)
484 .with_tooltip(self.tooltip_manager, txt, "", &bag_tooltip, TEXT_COLOR)
485 .set(state.ids.trade_details_btn, ui)
486 .was_clicked()
487 {
488 events.push(TradeEvent::SetDetailsMode(!self.show.trade_details));
489 }
490 }
491
492 let mut slot_maker = SlotMaker {
493 empty_slot: self.imgs.inv_slot,
494 filled_slot: self.imgs.inv_slot,
495 selected_slot: self.imgs.inv_slot_sel,
496 background_color: Some(UI_MAIN),
497 content_size: ContentSize {
498 width_height_ratio: 1.0,
499 max_fraction: 0.75,
500 },
501 selected_content_scale: 1.067,
502 amount_font: self.fonts.cyri.conrod_id,
503 amount_margins: Vec2::new(-4.0, 0.0),
504 amount_font_size: self.fonts.cyri.scale(12),
505 amount_text_color: TEXT_COLOR,
506 content_source: inventory,
507 image_source: self.item_imgs,
508 slot_manager: Some(self.slot_manager),
509 pulse: self.pulse,
510 };
511
512 if state.ids.inv_slots.len() < 2 * MAX_TRADE_SLOTS {
513 state.update(|s| {
514 s.ids
515 .inv_slots
516 .resize(2 * MAX_TRADE_SLOTS, &mut ui.widget_id_generator());
517 });
518 }
519
520 for i in 0..MAX_TRADE_SLOTS {
521 let x = i % 4;
522 let y = i / 4;
523
524 let slot = tradeslots.get(i).cloned().unwrap_or(TradeSlot {
525 index: i,
526 quantity: 0,
527 invslot: None,
528 ours,
529 entity,
530 });
531 let slot_widget = slot_maker
533 .fabricate(slot, [40.0; 2])
534 .top_left_with_margins_on(
535 state.ids.inv_alignment[who],
536 0.0 + y as f64 * (40.0),
537 0.0 + x as f64 * (40.0),
538 );
539 let slot_id = state.ids.inv_slots[i + who * MAX_TRADE_SLOTS];
540 if let Some(Some(item)) = slot.invslot.and_then(|slotid| inventory.slot(slotid)) {
541 let quality_col_img = match item.quality() {
542 Quality::Low => self.imgs.inv_slot_grey,
543 Quality::Common => self.imgs.inv_slot_common,
544 Quality::Moderate => self.imgs.inv_slot_green,
545 Quality::High => self.imgs.inv_slot_blue,
546 Quality::Epic => self.imgs.inv_slot_purple,
547 Quality::Legendary => self.imgs.inv_slot_gold,
548 Quality::Artifact => self.imgs.inv_slot_orange,
549 _ => self.imgs.inv_slot_red,
550 };
551
552 slot_widget
553 .filled_slot(quality_col_img)
554 .with_item_tooltip(
555 self.item_tooltip_manager,
556 core::iter::once(item as &dyn ItemDesc),
557 prices,
558 &item_tooltip,
559 )
560 .set(slot_id, ui);
561 } else {
562 slot_widget.set(slot_id, ui);
563 }
564 }
565 events
566 }
567
568 fn phase2_itemwidget(
569 &mut self,
570 state: &mut ConrodState<'_, State>,
571 ui: &mut UiCell<'_>,
572 inventory: &Inventory,
573 who: usize,
574 ours: bool,
575 entity: EcsEntity,
576 tradeslots: &[TradeSlot],
577 ) {
578 if state.ids.inv_textslots.len() < 2 * MAX_TRADE_SLOTS {
579 state.update(|s| {
580 s.ids
581 .inv_textslots
582 .resize(2 * MAX_TRADE_SLOTS, &mut ui.widget_id_generator());
583 });
584 }
585 let max_width = 170.0;
586 let mut total_text_height = 0.0;
587 let mut total_quantity = 0;
588 for i in 0..MAX_TRADE_SLOTS {
589 let slot = tradeslots.get(i).cloned().unwrap_or(TradeSlot {
590 index: i,
591 quantity: 0,
592 invslot: None,
593 ours,
594 entity,
595 });
596 total_quantity += slot.quantity;
597 let itemname = slot
598 .invslot
599 .and_then(|i| inventory.get(i))
600 .map(|i| {
601 let (name, _) = util::item_text(&i, self.localized_strings, self.item_i18n);
602
603 Cow::Owned(name)
604 })
605 .unwrap_or(Cow::Borrowed(""));
606 let is_present = slot.quantity > 0 && slot.invslot.is_some();
607 Text::new(&format!("{}x {}", slot.quantity, &itemname))
608 .top_left_with_margins_on(
609 state.ids.inv_alignment[who],
610 15.0 + i as f64 * 20.0 + total_text_height,
611 0.0,
612 )
613 .font_id(self.fonts.cyri.conrod_id)
614 .font_size(self.fonts.cyri.scale(20))
615 .wrap_by_word()
616 .w(max_width)
617 .color(Color::Rgba(
618 1.0,
619 1.0,
620 1.0,
621 if is_present { 1.0 } else { 0.0 },
622 ))
623 .set(state.ids.inv_textslots[i + who * MAX_TRADE_SLOTS], ui);
624 let label_height = match ui
625 .widget_graph()
626 .widget(state.ids.inv_textslots[i + who * MAX_TRADE_SLOTS])
627 .map(|widget| widget.rect)
628 {
629 Some(label_rect) => label_rect.h(),
630 None => 10.0,
631 };
632 total_text_height += label_height;
633 }
634 if total_quantity == 0 {
635 Text::new("Nothing!")
636 .top_left_with_margins_on(state.ids.inv_alignment[who], 10.0, 0.0)
637 .font_id(self.fonts.cyri.conrod_id)
638 .font_size(self.fonts.cyri.scale(20))
639 .color(Color::Rgba(
640 1.0,
641 0.25 + 0.25 * (4.0 * self.pulse).sin(),
642 0.0,
643 1.0,
644 ))
645 .set(state.ids.inv_textslots[who * MAX_TRADE_SLOTS], ui);
646
647 if !ours {
648 self.needs_thirdconfirm = true;
649 }
650 }
651 }
652
653 fn accept_decline_buttons(
654 &mut self,
655 state: &mut ConrodState<'_, State>,
656 ui: &mut UiCell<'_>,
657 trade: &'a PendingTrade,
658 ) -> Option<TradeEvent> {
659 let mut event = None;
660 let (hover_img, press_img, accept_button_luminance) = if trade.is_empty_trade() {
661 (
663 self.imgs.button,
664 self.imgs.button,
665 Color::Rgba(0.6, 0.6, 0.6, 1.0),
666 )
667 } else {
668 (
669 self.imgs.button_hover,
670 self.imgs.button_press,
671 Color::Rgba(1.0, 1.0, 1.0, 1.0),
672 )
673 };
674 if Button::image(self.imgs.button)
675 .w_h(31.0 * 5.0, 12.0 * 2.0)
676 .hover_image(hover_img)
677 .press_image(press_img)
678 .image_color(accept_button_luminance)
679 .bottom_left_with_margins_on(state.ids.bg, 90.0, 47.0)
680 .label(&self.localized_strings.get_msg("hud-trade-accept"))
681 .label_font_size(self.fonts.cyri.scale(14))
682 .label_color(TEXT_COLOR)
683 .label_font_id(self.fonts.cyri.conrod_id)
684 .label_y(Relative::Scalar(2.0))
685 .set(state.ids.accept_button, ui)
686 .was_clicked()
687 {
688 if matches!(trade.phase, TradePhase::Review) && self.needs_thirdconfirm {
689 event = Some(TradeEvent::ShowPrompt(PromptDialogSettings::new(
690 self.localized_strings
691 .get_msg("hud-confirm-trade-for-nothing")
692 .to_string(),
693 HudEvent::TradeAction(TradeAction::Accept(trade.phase())),
694 None,
695 )));
696 } else {
697 event = Some(TradeEvent::TradeAction(TradeAction::Accept(trade.phase())));
698 }
699 }
700
701 if Button::image(self.imgs.button)
702 .w_h(31.0 * 5.0, 12.0 * 2.0)
703 .hover_image(self.imgs.button_hover)
704 .press_image(self.imgs.button_press)
705 .right_from(state.ids.accept_button, 20.0)
706 .label(&self.localized_strings.get_msg("hud-trade-decline"))
707 .label_font_size(self.fonts.cyri.scale(14))
708 .label_color(TEXT_COLOR)
709 .label_font_id(self.fonts.cyri.conrod_id)
710 .label_y(Relative::Scalar(2.0))
711 .set(state.ids.decline_button, ui)
712 .was_clicked()
713 {
714 event = Some(TradeEvent::TradeAction(TradeAction::Decline));
715 }
716 event
717 }
718
719 fn input_item_amount(
720 &mut self,
721 state: &mut ConrodState<'_, State>,
722 ui: &mut UiCell<'_>,
723 trade: &'a PendingTrade,
724 ) -> Option<TradeEvent> {
725 let mut event = None;
726 let selected = self.slot_manager.selected().and_then(|s| match s {
727 SlotKind::Trade(t_s) => t_s.invslot.and_then(|slot| {
728 let who: usize = trade.offers[0].get(&slot).and(Some(0)).unwrap_or(1);
729 self.client
730 .inventories()
731 .get(t_s.entity)?
732 .get(slot)
733 .map(|item| (t_s.ours, slot, item.amount(), who))
734 }),
735 _ => None,
736 });
737 Rectangle::fill([132.0, 20.0])
738 .bottom_right_with_margins_on(state.ids.bg_frame, 16.0, 32.0)
739 .hsla(
740 0.0,
741 0.0,
742 0.0,
743 if self.show.trade_amount_input_key.is_some() {
744 0.75
745 } else {
746 0.35
747 },
748 )
749 .set(state.ids.amount_bg, ui);
750 if let Some((ours, slot, inv, who)) = selected {
751 self.show.trade_amount_input_key = None;
752 let input = trade.offers[who]
754 .get(&slot)
755 .map(|u| format!("{}", u))
756 .unwrap_or_else(String::new);
757 Text::new(&input)
758 .top_left_with_margins_on(state.ids.amount_bg, 0.0, 22.0)
759 .font_id(self.fonts.cyri.conrod_id)
760 .font_size(self.fonts.cyri.scale(14))
761 .color(TEXT_COLOR.alpha(0.7))
762 .set(state.ids.amount_open_label, ui);
763 if Button::image(self.imgs.edit_btn)
764 .hover_image(self.imgs.edit_btn_hover)
765 .press_image(self.imgs.edit_btn_press)
766 .mid_left_with_margin_on(state.ids.amount_bg, 2.0)
767 .w_h(16.0, 16.0)
768 .set(state.ids.amount_open_btn, ui)
769 .was_clicked()
770 {
771 event = Some(HudUpdate::Focus(state.ids.amount_input));
772 self.slot_manager.idle();
773 self.show.trade_amount_input_key =
774 Some(TradeAmountInput::new(slot, input, inv, ours, who));
775 }
776 Rectangle::fill_with([132.0, 20.0], color::TRANSPARENT)
777 .top_left_of(state.ids.amount_bg)
778 .graphics_for(state.ids.amount_open_btn)
779 .set(state.ids.amount_open_ovlay, ui);
780 } else if let Some(key) = &mut self.show.trade_amount_input_key {
781 if !Hud::is_captured::<TextEdit>(ui) && key.input_painted {
782 event = Some(HudUpdate::Submit);
784 }
785
786 if Button::image(self.imgs.close_btn)
787 .hover_image(self.imgs.close_btn_hover)
788 .press_image(self.imgs.close_btn_press)
789 .mid_left_with_margin_on(state.ids.amount_bg, 2.0)
790 .w_h(16.0, 16.0)
791 .set(state.ids.amount_btn, ui)
792 .was_clicked()
793 {
794 event = Some(HudUpdate::Submit);
795 }
796 key.input_painted = true;
798 let text_color = key.err.as_ref().and(Some(color::RED)).unwrap_or(TEXT_COLOR);
799 if let Some(new_input) = TextEdit::new(&key.input)
800 .mid_left_with_margin_on(state.ids.amount_bg, 22.0)
801 .w_h(138.0, 20.0)
802 .font_id(self.fonts.cyri.conrod_id)
803 .font_size(self.fonts.cyri.scale(14))
804 .color(text_color)
805 .set(state.ids.amount_input, ui)
806 && new_input != key.input
807 {
808 new_input.trim().clone_into(&mut key.input);
809 if !key.input.is_empty() {
810 let amount = *trade.offers[key.who].get(&key.slot).unwrap_or(&0);
812 match key.input.parse::<i32>() {
813 Ok(new_amount) => {
814 key.input = format!("{}", new_amount);
815 if new_amount > -1 && new_amount <= key.inv as i32 {
816 key.err = None;
817 let delta = new_amount - amount as i32;
818 key.submit_action = TradeAction::item(key.slot, delta, key.ours);
819 } else {
820 key.err = Some("out of range".to_owned());
821 key.submit_action = None;
822 }
823 },
824 Err(_) => {
825 key.err = Some("bad quantity".to_owned());
826 key.submit_action = None;
827 },
828 }
829 } else {
830 key.submit_action = None;
831 }
832 }
833 } else {
834 Text::new(&self.localized_strings.get_msg("hud-trade-amount_input"))
836 .middle_of(state.ids.amount_bg)
837 .font_id(self.fonts.cyri.conrod_id)
838 .font_size(self.fonts.cyri.scale(14))
839 .color(TEXT_GRAY_COLOR.alpha(0.25))
840 .set(state.ids.amount_notice, ui);
841 }
842 event.map(TradeEvent::HudUpdate)
843 }
844
845 fn close_button(
846 &mut self,
847 state: &mut ConrodState<'_, State>,
848 ui: &mut UiCell<'_>,
849 ) -> Option<TradeEvent> {
850 if Button::image(self.imgs.close_btn)
851 .w_h(24.0, 25.0)
852 .hover_image(self.imgs.close_btn_hover)
853 .press_image(self.imgs.close_btn_press)
854 .top_right_with_margins_on(state.ids.bg, 0.0, 0.0)
855 .set(state.ids.trade_close, ui)
856 .was_clicked()
857 {
858 Some(TradeEvent::TradeAction(TradeAction::Decline))
859 } else {
860 None
861 }
862 }
863}
864
865impl Widget for Trade<'_> {
866 type Event = Vec<TradeEvent>;
867 type State = State;
868 type Style = ();
869
870 fn init_state(&self, mut id_gen: widget::id::Generator) -> Self::State {
871 State {
872 bg_ids: BackgroundIds {
873 bg: id_gen.next(),
874 bg_frame: id_gen.next(),
875 },
876 ids: Ids::new(id_gen),
877 }
878 }
879
880 fn style(&self) -> Self::Style {}
881
882 fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
883 common_base::prof_span!("Trade::update");
884 let widget::UpdateArgs { state, ui, .. } = args;
885
886 let mut events = Vec::new();
887 let (trade, prices) = match self.client.pending_trade() {
888 Some((_, trade, prices)) => (trade, prices),
889 None => {
890 events.push(TradeEvent::TradeAction(TradeAction::Decline));
891 return events;
892 },
893 };
894
895 if state.ids.inv_alignment.len() < 2 {
896 state.update(|s| {
897 s.ids.inv_alignment.resize(2, &mut ui.widget_id_generator());
898 });
899 }
900 if state.ids.offer_headers.len() < 2 {
901 state.update(|s| {
902 s.ids.offer_headers.resize(2, &mut ui.widget_id_generator());
903 });
904 }
905 if state.ids.accept_indicators.len() < 2 {
906 state.update(|s| {
907 s.ids
908 .accept_indicators
909 .resize(2, &mut ui.widget_id_generator());
910 });
911 }
912
913 self.background(state, ui);
914 self.title(state, ui);
915 self.phase_indicator(state, ui, trade);
916
917 if let Some(event_list) = self.item_pane(state, ui, trade, prices, false) {
918 for event in event_list {
919 events.push(event);
920 }
921 }
922
923 if let Some(event_list) = self.item_pane(state, ui, trade, prices, true) {
924 for event in event_list {
925 events.push(event);
926 }
927 }
928
929 if let Some(event) = self.accept_decline_buttons(state, ui, trade) {
930 events.push(event);
931 }
932
933 if let Some(event) = self.close_button(state, ui) {
934 events.push(event);
935 }
936
937 if let Some(event) = self.input_item_amount(state, ui, trade) {
938 events.push(event);
939 }
940
941 events
942 }
943}