1use crate::{hud::animate_by_pulse, window::LastInput};
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 hovered_slot: image::Id,
36 pub filled_slot: image::Id,
37 pub selected_slot: image::Id,
38 pub background_color: Option<Color>,
40 pub content_size: ContentSize,
41 pub selected_content_scale: f32,
43 pub amount_font: font::Id,
44 pub amount_font_size: u32,
45 pub amount_margins: Vec2<f32>,
46 pub amount_text_color: Color,
47 pub content_source: &'a C,
48 pub image_source: &'a I,
49 pub slot_manager: Option<&'a mut SlotManager<S>>,
50 pub last_input: &'a LastInput,
51 pub pulse: f32,
52}
53
54impl<C, I, S> SlotMaker<'_, C, I, S>
55where
56 S: SumSlot,
57{
58 pub fn fabricate<K: SlotKey<C, I> + Into<S>>(
66 &mut self,
67 contents: K,
68 wh: [f32; 2],
69 menu_hover: bool,
70 menu_clicked: bool,
71 ) -> Slot<'_, K, C, I, S> {
72 let content_size = {
73 let ContentSize {
74 max_fraction,
75 width_height_ratio,
76 } = self.content_size;
77 let w_max = max_fraction * wh[0];
78 let h_max = max_fraction * wh[1];
79 let max_ratio = w_max / h_max;
80 let (w, h) = if max_ratio > width_height_ratio {
81 (width_height_ratio * h_max, w_max)
82 } else {
83 (w_max, w_max / width_height_ratio)
84 };
85 Vec2::new(w, h)
86 };
87 Slot::new(
88 contents,
89 self.empty_slot,
90 self.hovered_slot,
91 self.selected_slot,
92 self.filled_slot,
93 content_size,
94 self.selected_content_scale,
95 self.amount_font,
96 self.amount_font_size,
97 self.amount_margins,
98 self.amount_text_color,
99 self.content_source,
100 self.image_source,
101 menu_hover,
102 menu_clicked,
103 self.last_input,
104 self.pulse,
105 )
106 .wh([wh[0] as f64, wh[1] as f64])
107 .and_then(self.background_color, |s, c| s.with_background_color(c))
108 .and_then(self.slot_manager.as_mut(), |s, m| s.with_manager(m))
109 }
110}
111
112#[derive(Clone, Copy)]
113enum ManagerState<K> {
114 Dragging(
115 widget::Id,
116 K,
117 image::Id,
118 Option<u32>,
120 ),
121 Selected(widget::Id, K),
122 Idle,
123}
124
125enum Interaction {
126 Selected,
127 Dragging,
128 None,
129}
130
131pub enum Event<K> {
132 Dragged(K, K),
134 Dropped(K),
136 SplitDropped(K),
138 SplitDragged(K, K),
140 Used(K),
142 Request { slot: K, auto_quantity: bool },
144}
145pub struct SlotManager<S: SumSlot> {
147 state: ManagerState<S>,
148 slot_ids: Vec<widget::Id>,
150 slots: Vec<S>,
152 events: Vec<Event<S>>,
153 drag_id: widget::Id,
155 drag_img_size: Vec2<f32>,
158 pub mouse_over_slot: Option<S>,
159 use_prefixes: bool,
161 prefix_switch_point: u32,
162 }
183
184impl<S> SlotManager<S>
185where
186 S: SumSlot,
187{
188 pub fn new(
189 mut generator: widget::id::Generator,
190 drag_img_size: Vec2<f32>,
191 use_prefixes: bool,
192 prefix_switch_point: u32,
193 ) -> Self {
199 Self {
200 state: ManagerState::Idle,
201 slot_ids: Vec::new(),
202 slots: Vec::new(),
203 events: Vec::new(),
204 drag_id: generator.next(),
205 mouse_over_slot: None,
206 use_prefixes,
207 prefix_switch_point,
208 drag_img_size,
216 }
217 }
218
219 pub fn maintain(&mut self, ui: &mut conrod_core::UiCell) -> Vec<Event<S>> {
220 let slot_ids = core::mem::take(&mut self.slot_ids);
222 let slots = core::mem::take(&mut self.slots);
223
224 if let ManagerState::Selected(_, slot) = self.state
226 && ui.widget_input(ui.window).clicks().left().next().is_some()
227 {
228 self.state = ManagerState::Idle;
229 self.events.push(Event::Dropped(slot));
230 }
231
232 let input = &ui.global_input().current;
233 self.mouse_over_slot = input
234 .widget_under_mouse
235 .and_then(|x| slot_ids.iter().position(|slot_id| *slot_id == x))
236 .map(|x| slots[x]);
237
238 if let ManagerState::Dragging(_, slot, content_img, drag_amount) = &self.state {
241 let content_img = *content_img;
242 let drag_amount = *drag_amount;
243
244 let dragged_size = if let Some(dragged_size) = slot.drag_size() {
245 dragged_size
246 } else {
247 self.drag_img_size.map(|e| e as f64).into_array()
248 };
249
250 if drag_amount.is_some()
256 && let Some(id) = input.widget_under_mouse
257 && ui.widget_input(id).clicks().right().next().is_some()
258 {
259 if id == ui.window {
260 let temp_slot = *slot;
261 self.events.push(Event::SplitDropped(temp_slot));
262 } else if let Some(idx) = slot_ids.iter().position(|slot_id| *slot_id == id) {
263 let (from, to) = (*slot, slots[idx]);
264 if from != to {
265 self.events.push(Event::SplitDragged(from, to));
266 }
267 }
268 }
269
270 if let mouse::ButtonPosition::Up = input.mouse.buttons.left() {
271 if let Some(id) = input.widget_under_mouse {
273 if id == ui.window {
275 self.events.push(Event::Dropped(*slot));
276 } else if let Some(idx) = slot_ids.iter().position(|slot_id| *slot_id == id) {
277 let (from, to) = (*slot, slots[idx]);
279 if from != to {
281 self.events.push(Event::Dragged(from, to));
282 }
283 }
284 }
285 self.state = ManagerState::Idle;
287 }
288
289 let [mouse_x, mouse_y] = input.mouse.xy;
291 super::ghost_image::GhostImage::new(content_img)
292 .wh(dragged_size)
293 .no_parent()
294 .xy([mouse_x, mouse_y])
295 .set(self.drag_id, ui);
296
297 }
325
326 core::mem::take(&mut self.events)
327 }
328
329 pub fn set_use_prefixes(&mut self, use_prefixes: bool) { self.use_prefixes = use_prefixes; }
330
331 pub fn set_prefix_switch_point(&mut self, prefix_switch_point: u32) {
332 self.prefix_switch_point = prefix_switch_point;
333 }
334
335 fn update(
336 &mut self,
337 widget: widget::Id,
338 slot: S,
339 ui: &conrod_core::Ui,
340 content_img: Option<Vec<image::Id>>,
341 drag_amount: Option<u32>,
342 ) -> Interaction {
343 self.slot_ids.push(widget);
345 self.slots.push(slot);
346
347 let filled = content_img.is_some();
348 match &self.state {
350 ManagerState::Selected(id, _) | ManagerState::Dragging(id, _, _, _)
351 if *id == widget && !filled =>
352 {
353 self.state = ManagerState::Idle;
354 },
355 _ => (),
356 }
357
358 match &mut self.state {
360 ManagerState::Selected(id, stored_slot)
361 | ManagerState::Dragging(id, stored_slot, _, _)
362 if *id == widget =>
363 {
364 *stored_slot = slot
365 },
366 _ => (),
367 }
368
369 let input = ui.widget_input(widget);
370 let click_count = input.clicks().left().count();
371 if click_count > 0 {
372 self.state = if let ManagerState::Selected(id, other_slot) = self.state {
373 if id != widget {
374 if slot != other_slot {
376 self.events.push(Event::Dragged(other_slot, slot));
377 }
378 if click_count == 1 {
379 ManagerState::Idle
380 } else {
381 ManagerState::Selected(widget, slot)
382 }
383 } else {
384 ManagerState::Idle
386 }
387 } else {
388 if filled {
390 ManagerState::Selected(widget, slot)
391 } else {
392 ManagerState::Idle
394 }
395 };
396 }
397
398 if let Some(click) = input.clicks().left().next()
401 && !matches!(self.state, ManagerState::Dragging(_, _, _, _))
402 {
403 match click.modifiers {
404 ModifierKey::CTRL => {
405 self.events.push(Event::Request {
406 slot,
407 auto_quantity: true,
408 });
409 self.state = ManagerState::Idle;
410 },
411 ModifierKey::SHIFT => {
412 self.events.push(Event::Request {
413 slot,
414 auto_quantity: false,
415 });
416 self.state = ManagerState::Idle;
417 },
418 _ => {},
419 }
420 }
421
422 if input.clicks().right().next().is_some() {
424 match self.state {
425 ManagerState::Selected(_, _) | ManagerState::Idle => {
426 self.events.push(Event::Used(slot));
427 self.state = ManagerState::Idle;
429 },
430 ManagerState::Dragging(_, _, _, _) => {},
431 }
432 }
433
434 if input.drags().left().next().is_some()
436 && !matches!(self.state, ManagerState::Dragging(_, _, _, _))
437 {
438 if let Some(images) = content_img
440 && !images.is_empty()
441 {
442 self.state = ManagerState::Dragging(widget, slot, images[0], drag_amount);
443 }
444 }
445
446 match self.state {
448 ManagerState::Selected(id, _) if id == widget => Interaction::Selected,
449 ManagerState::Dragging(id, _, _, _) if id == widget => Interaction::Dragging,
450 _ => Interaction::None,
451 }
452 }
453
454 pub fn use_selected(&mut self) {
456 if let ManagerState::Selected(_, slot) = self.state {
457 self.events.push(Event::Used(slot));
458 self.state = ManagerState::Idle;
459 }
460 }
461
462 pub fn dropped_selected(&mut self) {
464 if let ManagerState::Selected(_, slot) = self.state {
465 self.events.push(Event::Dropped(slot));
466 self.state = ManagerState::Idle;
467 }
468 }
469
470 pub fn selected(&self) -> Option<S> {
472 if let ManagerState::Selected(_, s) = self.state {
473 Some(s)
474 } else {
475 None
476 }
477 }
478
479 pub fn select(&mut self, widget: widget::Id, slot: S) {
481 self.state = ManagerState::Selected(widget, slot);
484 }
485
486 pub fn idle(&mut self) { self.state = ManagerState::Idle; }
488}
489
490#[derive(WidgetCommon)]
491pub struct Slot<'a, K: SlotKey<C, I> + Into<S>, C, I, S: SumSlot> {
492 slot_key: K,
493
494 empty_slot: image::Id,
496 hovered_slot: image::Id,
497 selected_slot: image::Id,
498 background_color: Option<Color>,
499
500 content_size: Vec2<f32>,
502 selected_content_scale: f32,
503
504 icon: Option<(image::Id, Vec2<f32>, Option<Color>)>,
505
506 amount_font: font::Id,
508 amount_font_size: u32,
509 amount_margins: Vec2<f32>,
510 amount_text_color: Color,
511
512 slot_manager: Option<&'a mut SlotManager<S>>,
513 filled_slot: image::Id,
514 content_source: &'a C,
516 image_source: &'a I,
517
518 menu_hover: bool,
520 menu_click: bool,
521
522 last_input: &'a LastInput,
523
524 pulse: f32,
525
526 #[conrod(common_builder)]
527 common: widget::CommonBuilder,
528}
529
530widget_ids! {
531 struct Ids {
532 background,
533 icon,
534 amount,
535 amount_bg,
536 content,
537 slot_highlight,
538 }
539}
540
541pub struct State<K> {
543 ids: Ids,
544 cached_images: Option<(K, Vec<image::Id>)>,
545}
546
547impl<'a, K, C, I, S> Slot<'a, K, C, I, S>
548where
549 K: SlotKey<C, I> + Into<S>,
550 S: SumSlot,
551{
552 builder_methods! {
553 pub with_background_color { background_color = Some(Color) }
554 }
555
556 #[must_use]
557 pub fn with_manager(mut self, slot_manager: &'a mut SlotManager<S>) -> Self {
558 self.slot_manager = Some(slot_manager);
559 self
560 }
561
562 #[must_use]
563 pub fn filled_slot(mut self, img: image::Id) -> Self {
564 self.filled_slot = img;
565 self
566 }
567
568 #[must_use]
569 pub fn with_icon(mut self, img: image::Id, size: Vec2<f32>, color: Option<Color>) -> Self {
570 self.icon = Some((img, size, color));
571 self
572 }
573
574 #[expect(clippy::too_many_arguments)]
575 fn new(
576 slot_key: K,
577 empty_slot: image::Id,
578 hovered_slot: image::Id,
579 filled_slot: image::Id,
580 selected_slot: image::Id,
581 content_size: Vec2<f32>,
582 selected_content_scale: f32,
583 amount_font: font::Id,
584 amount_font_size: u32,
585 amount_margins: Vec2<f32>,
586 amount_text_color: Color,
587 content_source: &'a C,
588 image_source: &'a I,
589 menu_hover: bool,
590 menu_click: bool,
591 last_input: &'a LastInput,
592 pulse: f32,
593 ) -> Self {
594 Self {
595 slot_key,
596 empty_slot,
597 hovered_slot,
598 filled_slot,
599 selected_slot,
600 background_color: None,
601 content_size,
602 selected_content_scale,
603 icon: None,
604 amount_font,
605 amount_font_size,
606 amount_margins,
607 amount_text_color,
608 slot_manager: None,
609 content_source,
610 image_source,
611 menu_hover,
612 menu_click,
613 last_input,
614 pulse,
615 common: widget::CommonBuilder::default(),
616 }
617 }
618}
619
620impl<K, C, I, S> Widget for Slot<'_, K, C, I, S>
621where
622 K: SlotKey<C, I> + Into<S>,
623 S: SumSlot,
624{
625 type Event = ();
626 type State = State<K::ImageKey>;
627 type Style = ();
628
629 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
630 State {
631 ids: Ids::new(id_gen),
632 cached_images: None,
633 }
634 }
635
636 fn style(&self) -> Self::Style {}
637
638 fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
640 let widget::UpdateArgs {
641 id,
642 state,
643 rect,
644 ui,
645 ..
646 } = args;
647
648 let Slot {
649 slot_key,
650 empty_slot,
651 selected_slot,
652 background_color,
653 content_size,
654 selected_content_scale,
655 icon,
656 amount_font,
657 amount_font_size,
658 amount_margins,
659 amount_text_color,
660 content_source,
661 image_source,
662 ..
663 } = self;
664
665 let (image_key, content_color) = slot_key
667 .image_key(content_source)
668 .map_or((None, None), |(i, c)| (Some(i), c));
669 if state.cached_images.as_ref().map(|c| &c.0) != image_key.as_ref() {
670 state.update(|state| {
671 state.cached_images = image_key.map(|key| {
672 let image_ids = K::image_ids(&key, image_source);
673 (key, image_ids)
674 });
675 });
676 }
677
678 let content_images = state.cached_images.as_ref().map(|c| c.1.clone());
680
681 if self.menu_click && self.menu_hover {
683 self.slot_manager
684 .as_mut()
685 .map(|m| m.select(id, slot_key.into()));
686 }
687
688 let interaction = self.slot_manager.as_mut().map_or(Interaction::None, |m| {
690 m.update(
691 id,
692 slot_key.into(),
693 ui,
694 content_images.clone(),
695 slot_key.amount(content_source),
696 )
697 });
698 let content_images = if let Interaction::Dragging = interaction {
700 None
701 } else {
702 content_images
703 };
704 let slot_image = if let Interaction::Selected = interaction {
706 selected_slot
707 } else if content_images.is_some() {
708 self.filled_slot
709 } else {
710 empty_slot
711 };
712
713 let amount = if let Interaction::Dragging = interaction {
715 None } else {
717 slot_key.amount(content_source)
718 };
719
720 let (x, y, w, h) = rect.x_y_w_h();
722
723 Image::new(slot_image)
725 .x_y(x, y)
726 .w_h(w, h)
727 .parent(id)
728 .graphics_for(id)
729 .color(background_color)
730 .set(state.ids.background, ui);
731
732 if let (Some((icon_image, size, color)), true) = (icon, content_images.is_none()) {
735 let wh = size.map(|e| e as f64).into_array();
736 Image::new(icon_image)
737 .x_y(x, y)
738 .wh(wh)
739 .parent(id)
740 .graphics_for(id)
741 .color(color)
742 .set(state.ids.icon, ui);
743 }
744
745 if let Some(content_images) = content_images {
747 Image::new(animate_by_pulse(&content_images, self.pulse))
748 .x_y(x, y)
749 .wh((content_size
750 * if let Interaction::Selected = interaction {
751 selected_content_scale
752 } else {
753 1.0
754 })
755 .map(|e| e as f64)
756 .into_array())
757 .color(content_color)
758 .parent(id)
759 .graphics_for(id)
760 .set(state.ids.content, ui);
761 }
762
763 let is_highlighted = self
765 .slot_manager
766 .as_ref()
767 .map_or(false, |sm| sm.mouse_over_slot == Some(slot_key.into()))
768 && *self.last_input == LastInput::Mouse;
769
770 let is_highlighted_menu = self.menu_hover
773 && (*self.last_input == LastInput::Keyboard
774 || *self.last_input == LastInput::Controller);
775
776 if is_highlighted || is_highlighted_menu {
777 Image::new(self.hovered_slot)
778 .x_y(x, y)
779 .w_h(w, h)
780 .parent(id)
781 .graphics_for(id)
782 .set(state.ids.slot_highlight, ui);
783 }
784
785 if let Some(amount) = amount {
787 let amount = match self.slot_manager.as_ref().is_none_or(|sm| sm.use_prefixes) {
788 true => {
789 let threshold = amount
790 / (u32::pow(
791 10,
792 self.slot_manager
793 .map_or(4, |sm| sm.prefix_switch_point)
794 .saturating_sub(4),
795 ));
796 match amount {
797 amount if threshold >= 1_000_000_000 => {
798 format!("{}G", amount / 1_000_000_000)
799 },
800 amount if threshold >= 1_000_000 => format!("{}M", amount / 1_000_000),
801 amount if threshold >= 1_000 => format!("{}K", amount / 1_000),
802 amount => format!("{}", amount),
803 }
804 },
805 false => format!("{}", amount),
806 };
807 Text::new(&amount)
809 .font_id(amount_font)
810 .font_size(amount_font_size)
811 .bottom_right_with_margins_on(
812 state.ids.content,
813 amount_margins.x as f64,
814 amount_margins.y as f64,
815 )
816 .parent(id)
817 .graphics_for(id)
818 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
819 .set(state.ids.amount_bg, ui);
820 Text::new(&amount)
821 .parent(id)
822 .graphics_for(id)
823 .bottom_left_with_margins_on(
824 state.ids.amount_bg,
825 AMOUNT_SHADOW_OFFSET[0],
826 AMOUNT_SHADOW_OFFSET[1],
827 )
828 .font_id(amount_font)
829 .font_size(amount_font_size)
830 .color(amount_text_color)
831 .set(state.ids.amount, ui);
832 }
833 }
834}