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 .xy([mouse_x, mouse_y])
279 .set(self.drag_id, ui);
280
281 }
309
310 core::mem::take(&mut self.events)
311 }
312
313 pub fn set_use_prefixes(&mut self, use_prefixes: bool) { self.use_prefixes = use_prefixes; }
314
315 pub fn set_prefix_switch_point(&mut self, prefix_switch_point: u32) {
316 self.prefix_switch_point = prefix_switch_point;
317 }
318
319 fn update(
320 &mut self,
321 widget: widget::Id,
322 slot: S,
323 ui: &conrod_core::Ui,
324 content_img: Option<Vec<image::Id>>,
325 drag_amount: Option<u32>,
326 ) -> Interaction {
327 self.slot_ids.push(widget);
329 self.slots.push(slot);
330
331 let filled = content_img.is_some();
332 match &self.state {
334 ManagerState::Selected(id, _) | ManagerState::Dragging(id, _, _, _)
335 if *id == widget && !filled =>
336 {
337 self.state = ManagerState::Idle;
338 },
339 _ => (),
340 }
341
342 match &mut self.state {
344 ManagerState::Selected(id, stored_slot)
345 | ManagerState::Dragging(id, stored_slot, _, _)
346 if *id == widget =>
347 {
348 *stored_slot = slot
349 },
350 _ => (),
351 }
352
353 let input = ui.widget_input(widget);
354 let click_count = input.clicks().left().count();
357 if click_count > 0 {
358 let odd_num_clicks = click_count % 2 == 1;
359 self.state = if let ManagerState::Selected(id, other_slot) = self.state {
360 if id != widget {
361 if slot != other_slot {
363 self.events.push(Event::Dragged(other_slot, slot));
364 }
365 if click_count == 1 {
366 ManagerState::Idle
367 } else if click_count == 2 {
368 ManagerState::Selected(widget, slot)
370 } else {
371 self.events.push(Event::Used(slot));
373 ManagerState::Idle
374 }
375 } else {
376 self.events.push(Event::Used(slot));
379 ManagerState::Idle
380 }
381 } else {
382 if odd_num_clicks && filled {
384 ManagerState::Selected(widget, slot)
385 } else {
386 ManagerState::Idle
388 }
389 };
390 }
391
392 if let Some(click) = input.clicks().left().next()
395 && !matches!(self.state, ManagerState::Dragging(_, _, _, _))
396 {
397 match click.modifiers {
398 ModifierKey::CTRL => {
399 self.events.push(Event::Request {
400 slot,
401 auto_quantity: true,
402 });
403 self.state = ManagerState::Idle;
404 },
405 ModifierKey::SHIFT => {
406 self.events.push(Event::Request {
407 slot,
408 auto_quantity: false,
409 });
410 self.state = ManagerState::Idle;
411 },
412 _ => {},
413 }
414 }
415
416 if input.clicks().right().next().is_some() {
418 match self.state {
419 ManagerState::Selected(_, _) | ManagerState::Idle => {
420 self.events.push(Event::Used(slot));
421 self.state = ManagerState::Idle;
423 },
424 ManagerState::Dragging(_, _, _, _) => {},
425 }
426 }
427
428 if input.drags().left().next().is_some()
430 && !matches!(self.state, ManagerState::Dragging(_, _, _, _))
431 {
432 if let Some(images) = content_img
434 && !images.is_empty()
435 {
436 self.state = ManagerState::Dragging(widget, slot, images[0], drag_amount);
437 }
438 }
439
440 match self.state {
442 ManagerState::Selected(id, _) if id == widget => Interaction::Selected,
443 ManagerState::Dragging(id, _, _, _) if id == widget => Interaction::Dragging,
444 _ => Interaction::None,
445 }
446 }
447
448 pub fn selected(&self) -> Option<S> {
450 if let ManagerState::Selected(_, s) = self.state {
451 Some(s)
452 } else {
453 None
454 }
455 }
456
457 pub fn idle(&mut self) { self.state = ManagerState::Idle; }
459}
460
461#[derive(WidgetCommon)]
462pub struct Slot<'a, K: SlotKey<C, I> + Into<S>, C, I, S: SumSlot> {
463 slot_key: K,
464
465 empty_slot: image::Id,
467 selected_slot: image::Id,
468 background_color: Option<Color>,
469
470 content_size: Vec2<f32>,
472 selected_content_scale: f32,
473
474 icon: Option<(image::Id, Vec2<f32>, Option<Color>)>,
475
476 amount_font: font::Id,
478 amount_font_size: u32,
479 amount_margins: Vec2<f32>,
480 amount_text_color: Color,
481
482 slot_manager: Option<&'a mut SlotManager<S>>,
483 filled_slot: image::Id,
484 content_source: &'a C,
486 image_source: &'a I,
487
488 pulse: f32,
489
490 #[conrod(common_builder)]
491 common: widget::CommonBuilder,
492}
493
494widget_ids! {
495 struct Ids {
497 background,
498 icon,
499 amount,
500 amount_bg,
501 content,
502 }
503}
504
505pub struct State<K> {
507 ids: Ids,
508 cached_images: Option<(K, Vec<image::Id>)>,
509}
510
511impl<'a, K, C, I, S> Slot<'a, K, C, I, S>
512where
513 K: SlotKey<C, I> + Into<S>,
514 S: SumSlot,
515{
516 builder_methods! {
517 pub with_background_color { background_color = Some(Color) }
518 }
519
520 #[must_use]
521 pub fn with_manager(mut self, slot_manager: &'a mut SlotManager<S>) -> Self {
522 self.slot_manager = Some(slot_manager);
523 self
524 }
525
526 #[must_use]
527 pub fn filled_slot(mut self, img: image::Id) -> Self {
528 self.filled_slot = img;
529 self
530 }
531
532 #[must_use]
533 pub fn with_icon(mut self, img: image::Id, size: Vec2<f32>, color: Option<Color>) -> Self {
534 self.icon = Some((img, size, color));
535 self
536 }
537
538 fn new(
539 slot_key: K,
540 empty_slot: image::Id,
541 filled_slot: image::Id,
542 selected_slot: image::Id,
543 content_size: Vec2<f32>,
544 selected_content_scale: f32,
545 amount_font: font::Id,
546 amount_font_size: u32,
547 amount_margins: Vec2<f32>,
548 amount_text_color: Color,
549 content_source: &'a C,
550 image_source: &'a I,
551 pulse: f32,
552 ) -> Self {
553 Self {
554 slot_key,
555 empty_slot,
556 filled_slot,
557 selected_slot,
558 background_color: None,
559 content_size,
560 selected_content_scale,
561 icon: None,
562 amount_font,
563 amount_font_size,
564 amount_margins,
565 amount_text_color,
566 slot_manager: None,
567 content_source,
568 image_source,
569 pulse,
570 common: widget::CommonBuilder::default(),
571 }
572 }
573}
574
575impl<K, C, I, S> Widget for Slot<'_, K, C, I, S>
576where
577 K: SlotKey<C, I> + Into<S>,
578 S: SumSlot,
579{
580 type Event = ();
581 type State = State<K::ImageKey>;
582 type Style = ();
583
584 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
585 State {
586 ids: Ids::new(id_gen),
587 cached_images: None,
588 }
589 }
590
591 fn style(&self) -> Self::Style {}
592
593 fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
595 let widget::UpdateArgs {
596 id,
597 state,
598 rect,
599 ui,
600 ..
601 } = args;
602
603 let Slot {
604 slot_key,
605 empty_slot,
606 selected_slot,
607 background_color,
608 content_size,
609 selected_content_scale,
610 icon,
611 amount_font,
612 amount_font_size,
613 amount_margins,
614 amount_text_color,
615 content_source,
616 image_source,
617 ..
618 } = self;
619
620 let (image_key, content_color) = slot_key
622 .image_key(content_source)
623 .map_or((None, None), |(i, c)| (Some(i), c));
624 if state.cached_images.as_ref().map(|c| &c.0) != image_key.as_ref() {
625 state.update(|state| {
626 state.cached_images = image_key.map(|key| {
627 let image_ids = K::image_ids(&key, image_source);
628 (key, image_ids)
629 });
630 });
631 }
632
633 let content_images = state.cached_images.as_ref().map(|c| c.1.clone());
635 let interaction = self.slot_manager.as_mut().map_or(Interaction::None, |m| {
637 m.update(
638 id,
639 slot_key.into(),
640 ui,
641 content_images.clone(),
642 slot_key.amount(content_source),
643 )
644 });
645 let content_images = if let Interaction::Dragging = interaction {
647 None
648 } else {
649 content_images
650 };
651 let slot_image = if let Interaction::Selected = interaction {
653 selected_slot
654 } else if content_images.is_some() {
655 self.filled_slot
656 } else {
657 empty_slot
658 };
659
660 let amount = if let Interaction::Dragging = interaction {
662 None } else {
664 slot_key.amount(content_source)
665 };
666
667 let (x, y, w, h) = rect.x_y_w_h();
669
670 Image::new(slot_image)
672 .x_y(x, y)
673 .w_h(w, h)
674 .parent(id)
675 .graphics_for(id)
676 .color(background_color)
677 .set(state.ids.background, ui);
678
679 if let (Some((icon_image, size, color)), true) = (icon, content_images.is_none()) {
682 let wh = size.map(|e| e as f64).into_array();
683 Image::new(icon_image)
684 .x_y(x, y)
685 .wh(wh)
686 .parent(id)
687 .graphics_for(id)
688 .color(color)
689 .set(state.ids.icon, ui);
690 }
691
692 if let Some(content_images) = content_images {
694 Image::new(animate_by_pulse(&content_images, self.pulse))
695 .x_y(x, y)
696 .wh((content_size
697 * if let Interaction::Selected = interaction {
698 selected_content_scale
699 } else {
700 1.0
701 })
702 .map(|e| e as f64)
703 .into_array())
704 .color(content_color)
705 .parent(id)
706 .graphics_for(id)
707 .set(state.ids.content, ui);
708 }
709
710 if let Some(amount) = amount {
712 let amount = match self.slot_manager.as_ref().is_none_or(|sm| sm.use_prefixes) {
713 true => {
714 let threshold = amount
715 / (u32::pow(
716 10,
717 self.slot_manager
718 .map_or(4, |sm| sm.prefix_switch_point)
719 .saturating_sub(4),
720 ));
721 match amount {
722 amount if threshold >= 1_000_000_000 => {
723 format!("{}G", amount / 1_000_000_000)
724 },
725 amount if threshold >= 1_000_000 => format!("{}M", amount / 1_000_000),
726 amount if threshold >= 1_000 => format!("{}K", amount / 1_000),
727 amount => format!("{}", amount),
728 }
729 },
730 false => format!("{}", amount),
731 };
732 Text::new(&amount)
734 .font_id(amount_font)
735 .font_size(amount_font_size)
736 .bottom_right_with_margins_on(
737 state.ids.content,
738 amount_margins.x as f64,
739 amount_margins.y as f64,
740 )
741 .parent(id)
742 .graphics_for(id)
743 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
744 .set(state.ids.amount_bg, ui);
745 Text::new(&amount)
746 .parent(id)
747 .graphics_for(id)
748 .bottom_left_with_margins_on(
749 state.ids.amount_bg,
750 AMOUNT_SHADOW_OFFSET[0],
751 AMOUNT_SHADOW_OFFSET[1],
752 )
753 .font_id(amount_font)
754 .font_size(amount_font_size)
755 .color(amount_text_color)
756 .set(state.ids.amount, ui);
757 }
758 }
759}