1use crate::hud::animate_by_pulse;
3use conrod_core::{
4 Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, builder_methods, image,
5 input::{keyboard::ModifierKey, state::mouse},
6 text::font,
7 widget::{self, Image, Text},
8 widget_ids,
9};
10use vek::*;
11
12const AMOUNT_SHADOW_OFFSET: [f64; 2] = [1.0, 1.0];
13
14pub trait SlotKey<C, I>: Copy {
15 type ImageKey: PartialEq + Send + 'static;
16 fn image_key(&self, source: &C) -> Option<(Self::ImageKey, Option<Color>)>;
18 fn amount(&self, source: &C) -> Option<u32>;
19 fn image_ids(key: &Self::ImageKey, source: &I) -> Vec<image::Id>;
20}
21
22pub trait SumSlot: Sized + PartialEq + Copy + Send + 'static {
23 fn drag_size(&self) -> Option<[f64; 2]>;
24}
25
26pub struct ContentSize {
27 pub width_height_ratio: f32,
29 pub max_fraction: f32,
31}
32
33pub struct SlotMaker<'a, C, I, S: SumSlot> {
34 pub empty_slot: image::Id,
35 pub filled_slot: image::Id,
36 pub selected_slot: image::Id,
37 pub background_color: Option<Color>,
39 pub content_size: ContentSize,
40 pub selected_content_scale: f32,
42 pub amount_font: font::Id,
43 pub amount_font_size: u32,
44 pub amount_margins: Vec2<f32>,
45 pub amount_text_color: Color,
46 pub content_source: &'a C,
47 pub image_source: &'a I,
48 pub slot_manager: Option<&'a mut SlotManager<S>>,
49 pub pulse: f32,
50}
51
52impl<C, I, S> SlotMaker<'_, C, I, S>
53where
54 S: SumSlot,
55{
56 pub fn fabricate<K: SlotKey<C, I> + Into<S>>(
57 &mut self,
58 contents: K,
59 wh: [f32; 2],
60 ) -> Slot<'_, K, C, I, S> {
61 let content_size = {
62 let ContentSize {
63 max_fraction,
64 width_height_ratio,
65 } = self.content_size;
66 let w_max = max_fraction * wh[0];
67 let h_max = max_fraction * wh[1];
68 let max_ratio = w_max / h_max;
69 let (w, h) = if max_ratio > width_height_ratio {
70 (width_height_ratio * h_max, w_max)
71 } else {
72 (w_max, w_max / width_height_ratio)
73 };
74 Vec2::new(w, h)
75 };
76 Slot::new(
77 contents,
78 self.empty_slot,
79 self.selected_slot,
80 self.filled_slot,
81 content_size,
82 self.selected_content_scale,
83 self.amount_font,
84 self.amount_font_size,
85 self.amount_margins,
86 self.amount_text_color,
87 self.content_source,
88 self.image_source,
89 self.pulse,
90 )
91 .wh([wh[0] as f64, wh[1] as f64])
92 .and_then(self.background_color, |s, c| s.with_background_color(c))
93 .and_then(self.slot_manager.as_mut(), |s, m| s.with_manager(m))
94 }
95}
96
97#[derive(Clone, Copy)]
98enum ManagerState<K> {
99 Dragging(
100 widget::Id,
101 K,
102 image::Id,
103 Option<u32>,
105 ),
106 Selected(widget::Id, K),
107 Idle,
108}
109
110enum Interaction {
111 Selected,
112 Dragging,
113 None,
114}
115
116pub enum Event<K> {
117 Dragged(K, K),
119 Dropped(K),
121 SplitDropped(K),
123 SplitDragged(K, K),
125 Used(K),
127 Request { slot: K, auto_quantity: bool },
129}
130pub struct SlotManager<S: SumSlot> {
132 state: ManagerState<S>,
133 slot_ids: Vec<widget::Id>,
135 slots: Vec<S>,
137 events: Vec<Event<S>>,
138 drag_id: widget::Id,
140 drag_img_size: Vec2<f32>,
143 pub mouse_over_slot: Option<S>,
144 use_prefixes: bool,
146 prefix_switch_point: u32,
147 }
168
169impl<S> SlotManager<S>
170where
171 S: SumSlot,
172{
173 pub fn new(
174 mut generator: widget::id::Generator,
175 drag_img_size: Vec2<f32>,
176 use_prefixes: bool,
177 prefix_switch_point: u32,
178 ) -> Self {
184 Self {
185 state: ManagerState::Idle,
186 slot_ids: Vec::new(),
187 slots: Vec::new(),
188 events: Vec::new(),
189 drag_id: generator.next(),
190 mouse_over_slot: None,
191 use_prefixes,
192 prefix_switch_point,
193 drag_img_size,
201 }
202 }
203
204 pub fn maintain(&mut self, ui: &mut conrod_core::UiCell) -> Vec<Event<S>> {
205 let slot_ids = core::mem::take(&mut self.slot_ids);
207 let slots = core::mem::take(&mut self.slots);
208
209 if let ManagerState::Selected(_, slot) = self.state
211 && ui.widget_input(ui.window).clicks().left().next().is_some()
212 {
213 self.state = ManagerState::Idle;
214 self.events.push(Event::Dropped(slot));
215 }
216
217 let input = &ui.global_input().current;
218 self.mouse_over_slot = input
219 .widget_under_mouse
220 .and_then(|x| slot_ids.iter().position(|slot_id| *slot_id == x))
221 .map(|x| slots[x]);
222
223 if let ManagerState::Dragging(_, slot, content_img, drag_amount) = &self.state {
226 let content_img = *content_img;
227 let drag_amount = *drag_amount;
228
229 let dragged_size = if let Some(dragged_size) = slot.drag_size() {
230 dragged_size
231 } else {
232 self.drag_img_size.map(|e| e as f64).into_array()
233 };
234
235 if drag_amount.is_some()
241 && let Some(id) = input.widget_under_mouse
242 && ui.widget_input(id).clicks().right().next().is_some()
243 {
244 if id == ui.window {
245 let temp_slot = *slot;
246 self.events.push(Event::SplitDropped(temp_slot));
247 } else if let Some(idx) = slot_ids.iter().position(|slot_id| *slot_id == id) {
248 let (from, to) = (*slot, slots[idx]);
249 if from != to {
250 self.events.push(Event::SplitDragged(from, to));
251 }
252 }
253 }
254
255 if let mouse::ButtonPosition::Up = input.mouse.buttons.left() {
256 if let Some(id) = input.widget_under_mouse {
258 if id == ui.window {
260 self.events.push(Event::Dropped(*slot));
261 } else if let Some(idx) = slot_ids.iter().position(|slot_id| *slot_id == id) {
262 let (from, to) = (*slot, slots[idx]);
264 if from != to {
266 self.events.push(Event::Dragged(from, to));
267 }
268 }
269 }
270 self.state = ManagerState::Idle;
272 }
273
274 let [mouse_x, mouse_y] = input.mouse.xy;
276 super::ghost_image::GhostImage::new(content_img)
277 .wh(dragged_size)
278 .no_parent()
279 .xy([mouse_x, mouse_y])
280 .set(self.drag_id, ui);
281
282 }
310
311 core::mem::take(&mut self.events)
312 }
313
314 pub fn set_use_prefixes(&mut self, use_prefixes: bool) { self.use_prefixes = use_prefixes; }
315
316 pub fn set_prefix_switch_point(&mut self, prefix_switch_point: u32) {
317 self.prefix_switch_point = prefix_switch_point;
318 }
319
320 fn update(
321 &mut self,
322 widget: widget::Id,
323 slot: S,
324 ui: &conrod_core::Ui,
325 content_img: Option<Vec<image::Id>>,
326 drag_amount: Option<u32>,
327 ) -> Interaction {
328 self.slot_ids.push(widget);
330 self.slots.push(slot);
331
332 let filled = content_img.is_some();
333 match &self.state {
335 ManagerState::Selected(id, _) | ManagerState::Dragging(id, _, _, _)
336 if *id == widget && !filled =>
337 {
338 self.state = ManagerState::Idle;
339 },
340 _ => (),
341 }
342
343 match &mut self.state {
345 ManagerState::Selected(id, stored_slot)
346 | ManagerState::Dragging(id, stored_slot, _, _)
347 if *id == widget =>
348 {
349 *stored_slot = slot
350 },
351 _ => (),
352 }
353
354 let input = ui.widget_input(widget);
355 let click_count = input.clicks().left().count();
358 if click_count > 0 {
359 let odd_num_clicks = click_count % 2 == 1;
360 self.state = if let ManagerState::Selected(id, other_slot) = self.state {
361 if id != widget {
362 if slot != other_slot {
364 self.events.push(Event::Dragged(other_slot, slot));
365 }
366 if click_count == 1 {
367 ManagerState::Idle
368 } else if click_count == 2 {
369 ManagerState::Selected(widget, slot)
371 } else {
372 self.events.push(Event::Used(slot));
374 ManagerState::Idle
375 }
376 } else {
377 self.events.push(Event::Used(slot));
380 ManagerState::Idle
381 }
382 } else {
383 if odd_num_clicks && filled {
385 ManagerState::Selected(widget, slot)
386 } else {
387 ManagerState::Idle
389 }
390 };
391 }
392
393 if let Some(click) = input.clicks().left().next()
396 && !matches!(self.state, ManagerState::Dragging(_, _, _, _))
397 {
398 match click.modifiers {
399 ModifierKey::CTRL => {
400 self.events.push(Event::Request {
401 slot,
402 auto_quantity: true,
403 });
404 self.state = ManagerState::Idle;
405 },
406 ModifierKey::SHIFT => {
407 self.events.push(Event::Request {
408 slot,
409 auto_quantity: false,
410 });
411 self.state = ManagerState::Idle;
412 },
413 _ => {},
414 }
415 }
416
417 if input.clicks().right().next().is_some() {
419 match self.state {
420 ManagerState::Selected(_, _) | ManagerState::Idle => {
421 self.events.push(Event::Used(slot));
422 self.state = ManagerState::Idle;
424 },
425 ManagerState::Dragging(_, _, _, _) => {},
426 }
427 }
428
429 if input.drags().left().next().is_some()
431 && !matches!(self.state, ManagerState::Dragging(_, _, _, _))
432 {
433 if let Some(images) = content_img
435 && !images.is_empty()
436 {
437 self.state = ManagerState::Dragging(widget, slot, images[0], drag_amount);
438 }
439 }
440
441 match self.state {
443 ManagerState::Selected(id, _) if id == widget => Interaction::Selected,
444 ManagerState::Dragging(id, _, _, _) if id == widget => Interaction::Dragging,
445 _ => Interaction::None,
446 }
447 }
448
449 pub fn selected(&self) -> Option<S> {
451 if let ManagerState::Selected(_, s) = self.state {
452 Some(s)
453 } else {
454 None
455 }
456 }
457
458 pub fn idle(&mut self) { self.state = ManagerState::Idle; }
460}
461
462#[derive(WidgetCommon)]
463pub struct Slot<'a, K: SlotKey<C, I> + Into<S>, C, I, S: SumSlot> {
464 slot_key: K,
465
466 empty_slot: image::Id,
468 selected_slot: image::Id,
469 background_color: Option<Color>,
470
471 content_size: Vec2<f32>,
473 selected_content_scale: f32,
474
475 icon: Option<(image::Id, Vec2<f32>, Option<Color>)>,
476
477 amount_font: font::Id,
479 amount_font_size: u32,
480 amount_margins: Vec2<f32>,
481 amount_text_color: Color,
482
483 slot_manager: Option<&'a mut SlotManager<S>>,
484 filled_slot: image::Id,
485 content_source: &'a C,
487 image_source: &'a I,
488
489 pulse: f32,
490
491 #[conrod(common_builder)]
492 common: widget::CommonBuilder,
493}
494
495widget_ids! {
496 struct Ids {
498 background,
499 icon,
500 amount,
501 amount_bg,
502 content,
503 }
504}
505
506pub struct State<K> {
508 ids: Ids,
509 cached_images: Option<(K, Vec<image::Id>)>,
510}
511
512impl<'a, K, C, I, S> Slot<'a, K, C, I, S>
513where
514 K: SlotKey<C, I> + Into<S>,
515 S: SumSlot,
516{
517 builder_methods! {
518 pub with_background_color { background_color = Some(Color) }
519 }
520
521 #[must_use]
522 pub fn with_manager(mut self, slot_manager: &'a mut SlotManager<S>) -> Self {
523 self.slot_manager = Some(slot_manager);
524 self
525 }
526
527 #[must_use]
528 pub fn filled_slot(mut self, img: image::Id) -> Self {
529 self.filled_slot = img;
530 self
531 }
532
533 #[must_use]
534 pub fn with_icon(mut self, img: image::Id, size: Vec2<f32>, color: Option<Color>) -> Self {
535 self.icon = Some((img, size, color));
536 self
537 }
538
539 fn new(
540 slot_key: K,
541 empty_slot: image::Id,
542 filled_slot: image::Id,
543 selected_slot: image::Id,
544 content_size: Vec2<f32>,
545 selected_content_scale: f32,
546 amount_font: font::Id,
547 amount_font_size: u32,
548 amount_margins: Vec2<f32>,
549 amount_text_color: Color,
550 content_source: &'a C,
551 image_source: &'a I,
552 pulse: f32,
553 ) -> Self {
554 Self {
555 slot_key,
556 empty_slot,
557 filled_slot,
558 selected_slot,
559 background_color: None,
560 content_size,
561 selected_content_scale,
562 icon: None,
563 amount_font,
564 amount_font_size,
565 amount_margins,
566 amount_text_color,
567 slot_manager: None,
568 content_source,
569 image_source,
570 pulse,
571 common: widget::CommonBuilder::default(),
572 }
573 }
574}
575
576impl<K, C, I, S> Widget for Slot<'_, K, C, I, S>
577where
578 K: SlotKey<C, I> + Into<S>,
579 S: SumSlot,
580{
581 type Event = ();
582 type State = State<K::ImageKey>;
583 type Style = ();
584
585 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
586 State {
587 ids: Ids::new(id_gen),
588 cached_images: None,
589 }
590 }
591
592 fn style(&self) -> Self::Style {}
593
594 fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
596 let widget::UpdateArgs {
597 id,
598 state,
599 rect,
600 ui,
601 ..
602 } = args;
603
604 let Slot {
605 slot_key,
606 empty_slot,
607 selected_slot,
608 background_color,
609 content_size,
610 selected_content_scale,
611 icon,
612 amount_font,
613 amount_font_size,
614 amount_margins,
615 amount_text_color,
616 content_source,
617 image_source,
618 ..
619 } = self;
620
621 let (image_key, content_color) = slot_key
623 .image_key(content_source)
624 .map_or((None, None), |(i, c)| (Some(i), c));
625 if state.cached_images.as_ref().map(|c| &c.0) != image_key.as_ref() {
626 state.update(|state| {
627 state.cached_images = image_key.map(|key| {
628 let image_ids = K::image_ids(&key, image_source);
629 (key, image_ids)
630 });
631 });
632 }
633
634 let content_images = state.cached_images.as_ref().map(|c| c.1.clone());
636 let interaction = self.slot_manager.as_mut().map_or(Interaction::None, |m| {
638 m.update(
639 id,
640 slot_key.into(),
641 ui,
642 content_images.clone(),
643 slot_key.amount(content_source),
644 )
645 });
646 let content_images = if let Interaction::Dragging = interaction {
648 None
649 } else {
650 content_images
651 };
652 let slot_image = if let Interaction::Selected = interaction {
654 selected_slot
655 } else if content_images.is_some() {
656 self.filled_slot
657 } else {
658 empty_slot
659 };
660
661 let amount = if let Interaction::Dragging = interaction {
663 None } else {
665 slot_key.amount(content_source)
666 };
667
668 let (x, y, w, h) = rect.x_y_w_h();
670
671 Image::new(slot_image)
673 .x_y(x, y)
674 .w_h(w, h)
675 .parent(id)
676 .graphics_for(id)
677 .color(background_color)
678 .set(state.ids.background, ui);
679
680 if let (Some((icon_image, size, color)), true) = (icon, content_images.is_none()) {
683 let wh = size.map(|e| e as f64).into_array();
684 Image::new(icon_image)
685 .x_y(x, y)
686 .wh(wh)
687 .parent(id)
688 .graphics_for(id)
689 .color(color)
690 .set(state.ids.icon, ui);
691 }
692
693 if let Some(content_images) = content_images {
695 Image::new(animate_by_pulse(&content_images, self.pulse))
696 .x_y(x, y)
697 .wh((content_size
698 * if let Interaction::Selected = interaction {
699 selected_content_scale
700 } else {
701 1.0
702 })
703 .map(|e| e as f64)
704 .into_array())
705 .color(content_color)
706 .parent(id)
707 .graphics_for(id)
708 .set(state.ids.content, ui);
709 }
710
711 if let Some(amount) = amount {
713 let amount = match self.slot_manager.as_ref().is_none_or(|sm| sm.use_prefixes) {
714 true => {
715 let threshold = amount
716 / (u32::pow(
717 10,
718 self.slot_manager
719 .map_or(4, |sm| sm.prefix_switch_point)
720 .saturating_sub(4),
721 ));
722 match amount {
723 amount if threshold >= 1_000_000_000 => {
724 format!("{}G", amount / 1_000_000_000)
725 },
726 amount if threshold >= 1_000_000 => format!("{}M", amount / 1_000_000),
727 amount if threshold >= 1_000 => format!("{}K", amount / 1_000),
728 amount => format!("{}", amount),
729 }
730 },
731 false => format!("{}", amount),
732 };
733 Text::new(&amount)
735 .font_id(amount_font)
736 .font_size(amount_font_size)
737 .bottom_right_with_margins_on(
738 state.ids.content,
739 amount_margins.x as f64,
740 amount_margins.y as f64,
741 )
742 .parent(id)
743 .graphics_for(id)
744 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
745 .set(state.ids.amount_bg, ui);
746 Text::new(&amount)
747 .parent(id)
748 .graphics_for(id)
749 .bottom_left_with_margins_on(
750 state.ids.amount_bg,
751 AMOUNT_SHADOW_OFFSET[0],
752 AMOUNT_SHADOW_OFFSET[1],
753 )
754 .font_id(amount_font)
755 .font_size(amount_font_size)
756 .color(amount_text_color)
757 .set(state.ids.amount, ui);
758 }
759 }
760}