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 &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 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 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 (
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 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 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 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 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 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}