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 gen: 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: gen.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 if ui.widget_input(ui.window).clicks().left().next().is_some() {
212 self.state = ManagerState::Idle;
213 self.events.push(Event::Dropped(slot));
214 }
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 if let Some(id) = input.widget_under_mouse {
242 if ui.widget_input(id).clicks().right().next().is_some() {
243 if id == ui.window {
244 let temp_slot = *slot;
245 self.events.push(Event::SplitDropped(temp_slot));
246 } else if let Some(idx) = slot_ids.iter().position(|slot_id| *slot_id == id)
247 {
248 let (from, to) = (*slot, slots[idx]);
249 if from != to {
250 self.events.push(Event::SplitDragged(from, to));
251 }
252 }
253 }
254 }
255 }
256
257 if let mouse::ButtonPosition::Up = input.mouse.buttons.left() {
258 if let Some(id) = input.widget_under_mouse {
260 if id == ui.window {
262 self.events.push(Event::Dropped(*slot));
263 } else if let Some(idx) = slot_ids.iter().position(|slot_id| *slot_id == id) {
264 let (from, to) = (*slot, slots[idx]);
266 if from != to {
268 self.events.push(Event::Dragged(from, to));
269 }
270 }
271 }
272 self.state = ManagerState::Idle;
274 }
275
276 let [mouse_x, mouse_y] = input.mouse.xy;
278 super::ghost_image::GhostImage::new(content_img)
279 .wh(dragged_size)
280 .xy([mouse_x, mouse_y])
281 .set(self.drag_id, ui);
282
283 }
311
312 core::mem::take(&mut self.events)
313 }
314
315 pub fn set_use_prefixes(&mut self, use_prefixes: bool) { self.use_prefixes = use_prefixes; }
316
317 pub fn set_prefix_switch_point(&mut self, prefix_switch_point: u32) {
318 self.prefix_switch_point = prefix_switch_point;
319 }
320
321 fn update(
322 &mut self,
323 widget: widget::Id,
324 slot: S,
325 ui: &conrod_core::Ui,
326 content_img: Option<Vec<image::Id>>,
327 drag_amount: Option<u32>,
328 ) -> Interaction {
329 self.slot_ids.push(widget);
331 self.slots.push(slot);
332
333 let filled = content_img.is_some();
334 match &self.state {
336 ManagerState::Selected(id, _) | ManagerState::Dragging(id, _, _, _)
337 if *id == widget && !filled =>
338 {
339 self.state = ManagerState::Idle;
340 },
341 _ => (),
342 }
343
344 match &mut self.state {
346 ManagerState::Selected(id, stored_slot)
347 | ManagerState::Dragging(id, stored_slot, _, _)
348 if *id == widget =>
349 {
350 *stored_slot = slot
351 },
352 _ => (),
353 }
354
355 let input = ui.widget_input(widget);
356 let click_count = input.clicks().left().count();
359 if click_count > 0 {
360 let odd_num_clicks = click_count % 2 == 1;
361 self.state = if let ManagerState::Selected(id, other_slot) = self.state {
362 if id != widget {
363 if slot != other_slot {
365 self.events.push(Event::Dragged(other_slot, slot));
366 }
367 if click_count == 1 {
368 ManagerState::Idle
369 } else if click_count == 2 {
370 ManagerState::Selected(widget, slot)
372 } else {
373 self.events.push(Event::Used(slot));
375 ManagerState::Idle
376 }
377 } else {
378 self.events.push(Event::Used(slot));
381 ManagerState::Idle
382 }
383 } else {
384 if odd_num_clicks && filled {
386 ManagerState::Selected(widget, slot)
387 } else {
388 ManagerState::Idle
390 }
391 };
392 }
393
394 if let Some(click) = input.clicks().left().next() {
397 if !matches!(self.state, ManagerState::Dragging(_, _, _, _)) {
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
418 if input.clicks().right().next().is_some() {
420 match self.state {
421 ManagerState::Selected(_, _) | ManagerState::Idle => {
422 self.events.push(Event::Used(slot));
423 self.state = ManagerState::Idle;
425 },
426 ManagerState::Dragging(_, _, _, _) => {},
427 }
428 }
429
430 if input.drags().left().next().is_some()
432 && !matches!(self.state, ManagerState::Dragging(_, _, _, _))
433 {
434 if let Some(images) = content_img {
436 if !images.is_empty() {
437 self.state = ManagerState::Dragging(widget, slot, images[0], drag_amount);
438 }
439 }
440 }
441
442 match self.state {
444 ManagerState::Selected(id, _) if id == widget => Interaction::Selected,
445 ManagerState::Dragging(id, _, _, _) if id == widget => Interaction::Dragging,
446 _ => Interaction::None,
447 }
448 }
449
450 pub fn selected(&self) -> Option<S> {
452 if let ManagerState::Selected(_, s) = self.state {
453 Some(s)
454 } else {
455 None
456 }
457 }
458
459 pub fn idle(&mut self) { self.state = ManagerState::Idle; }
461}
462
463#[derive(WidgetCommon)]
464pub struct Slot<'a, K: SlotKey<C, I> + Into<S>, C, I, S: SumSlot> {
465 slot_key: K,
466
467 empty_slot: image::Id,
469 selected_slot: image::Id,
470 background_color: Option<Color>,
471
472 content_size: Vec2<f32>,
474 selected_content_scale: f32,
475
476 icon: Option<(image::Id, Vec2<f32>, Option<Color>)>,
477
478 amount_font: font::Id,
480 amount_font_size: u32,
481 amount_margins: Vec2<f32>,
482 amount_text_color: Color,
483
484 slot_manager: Option<&'a mut SlotManager<S>>,
485 filled_slot: image::Id,
486 content_source: &'a C,
488 image_source: &'a I,
489
490 pulse: f32,
491
492 #[conrod(common_builder)]
493 common: widget::CommonBuilder,
494}
495
496widget_ids! {
497 struct Ids {
499 background,
500 icon,
501 amount,
502 amount_bg,
503 content,
504 }
505}
506
507pub struct State<K> {
509 ids: Ids,
510 cached_images: Option<(K, Vec<image::Id>)>,
511}
512
513impl<'a, K, C, I, S> Slot<'a, K, C, I, S>
514where
515 K: SlotKey<C, I> + Into<S>,
516 S: SumSlot,
517{
518 builder_methods! {
519 pub with_background_color { background_color = Some(Color) }
520 }
521
522 #[must_use]
523 pub fn with_manager(mut self, slot_manager: &'a mut SlotManager<S>) -> Self {
524 self.slot_manager = Some(slot_manager);
525 self
526 }
527
528 #[must_use]
529 pub fn filled_slot(mut self, img: image::Id) -> Self {
530 self.filled_slot = img;
531 self
532 }
533
534 #[must_use]
535 pub fn with_icon(mut self, img: image::Id, size: Vec2<f32>, color: Option<Color>) -> Self {
536 self.icon = Some((img, size, color));
537 self
538 }
539
540 fn new(
541 slot_key: K,
542 empty_slot: image::Id,
543 filled_slot: image::Id,
544 selected_slot: image::Id,
545 content_size: Vec2<f32>,
546 selected_content_scale: f32,
547 amount_font: font::Id,
548 amount_font_size: u32,
549 amount_margins: Vec2<f32>,
550 amount_text_color: Color,
551 content_source: &'a C,
552 image_source: &'a I,
553 pulse: f32,
554 ) -> Self {
555 Self {
556 slot_key,
557 empty_slot,
558 filled_slot,
559 selected_slot,
560 background_color: None,
561 content_size,
562 selected_content_scale,
563 icon: None,
564 amount_font,
565 amount_font_size,
566 amount_margins,
567 amount_text_color,
568 slot_manager: None,
569 content_source,
570 image_source,
571 pulse,
572 common: widget::CommonBuilder::default(),
573 }
574 }
575}
576
577impl<K, C, I, S> Widget for Slot<'_, K, C, I, S>
578where
579 K: SlotKey<C, I> + Into<S>,
580 S: SumSlot,
581{
582 type Event = ();
583 type State = State<K::ImageKey>;
584 type Style = ();
585
586 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
587 State {
588 ids: Ids::new(id_gen),
589 cached_images: None,
590 }
591 }
592
593 fn style(&self) -> Self::Style {}
594
595 fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
597 let widget::UpdateArgs {
598 id,
599 state,
600 rect,
601 ui,
602 ..
603 } = args;
604
605 let Slot {
606 slot_key,
607 empty_slot,
608 selected_slot,
609 background_color,
610 content_size,
611 selected_content_scale,
612 icon,
613 amount_font,
614 amount_font_size,
615 amount_margins,
616 amount_text_color,
617 content_source,
618 image_source,
619 ..
620 } = self;
621
622 let (image_key, content_color) = slot_key
624 .image_key(content_source)
625 .map_or((None, None), |(i, c)| (Some(i), c));
626 if state.cached_images.as_ref().map(|c| &c.0) != image_key.as_ref() {
627 state.update(|state| {
628 state.cached_images = image_key.map(|key| {
629 let image_ids = K::image_ids(&key, image_source);
630 (key, image_ids)
631 });
632 });
633 }
634
635 let content_images = state.cached_images.as_ref().map(|c| c.1.clone());
637 let interaction = self.slot_manager.as_mut().map_or(Interaction::None, |m| {
639 m.update(
640 id,
641 slot_key.into(),
642 ui,
643 content_images.clone(),
644 slot_key.amount(content_source),
645 )
646 });
647 let content_images = if let Interaction::Dragging = interaction {
649 None
650 } else {
651 content_images
652 };
653 let slot_image = if let Interaction::Selected = interaction {
655 selected_slot
656 } else if content_images.is_some() {
657 self.filled_slot
658 } else {
659 empty_slot
660 };
661
662 let amount = if let Interaction::Dragging = interaction {
664 None } else {
666 slot_key.amount(content_source)
667 };
668
669 let (x, y, w, h) = rect.x_y_w_h();
671
672 Image::new(slot_image)
674 .x_y(x, y)
675 .w_h(w, h)
676 .parent(id)
677 .graphics_for(id)
678 .color(background_color)
679 .set(state.ids.background, ui);
680
681 if let (Some((icon_image, size, color)), true) = (icon, content_images.is_none()) {
684 let wh = size.map(|e| e as f64).into_array();
685 Image::new(icon_image)
686 .x_y(x, y)
687 .wh(wh)
688 .parent(id)
689 .graphics_for(id)
690 .color(color)
691 .set(state.ids.icon, ui);
692 }
693
694 if let Some(content_images) = content_images {
696 Image::new(animate_by_pulse(&content_images, self.pulse))
697 .x_y(x, y)
698 .wh((content_size
699 * if let Interaction::Selected = interaction {
700 selected_content_scale
701 } else {
702 1.0
703 })
704 .map(|e| e as f64)
705 .into_array())
706 .color(content_color)
707 .parent(id)
708 .graphics_for(id)
709 .set(state.ids.content, ui);
710 }
711
712 if let Some(amount) = amount {
714 let amount = match self.slot_manager.as_ref().is_none_or(|sm| sm.use_prefixes) {
715 true => {
716 let threshold = amount
717 / (u32::pow(
718 10,
719 self.slot_manager
720 .map_or(4, |sm| sm.prefix_switch_point)
721 .saturating_sub(4),
722 ));
723 match amount {
724 amount if threshold >= 1_000_000_000 => {
725 format!("{}G", amount / 1_000_000_000)
726 },
727 amount if threshold >= 1_000_000 => format!("{}M", amount / 1_000_000),
728 amount if threshold >= 1_000 => format!("{}K", amount / 1_000),
729 amount => format!("{}", amount),
730 }
731 },
732 false => format!("{}", amount),
733 };
734 Text::new(&amount)
736 .font_id(amount_font)
737 .font_size(amount_font_size)
738 .bottom_right_with_margins_on(
739 state.ids.content,
740 amount_margins.x as f64,
741 amount_margins.y as f64,
742 )
743 .parent(id)
744 .graphics_for(id)
745 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
746 .set(state.ids.amount_bg, ui);
747 Text::new(&amount)
748 .parent(id)
749 .graphics_for(id)
750 .bottom_left_with_margins_on(
751 state.ids.amount_bg,
752 AMOUNT_SHADOW_OFFSET[0],
753 AMOUNT_SHADOW_OFFSET[1],
754 )
755 .font_id(amount_font)
756 .font_size(amount_font_size)
757 .color(amount_text_color)
758 .set(state.ids.amount, ui);
759 }
760 }
761}