veloren_voxygen/ui/widgets/
slot.rs

1//! A widget for selecting a single value along some linear range.
2use 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    /// Returns an Option since the slot could be empty
17    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    // Width divided by height
28    pub width_height_ratio: f32,
29    // Max fraction of slot widget size that each side can be
30    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    // Is this useful?
38    pub background_color: Option<Color>,
39    pub content_size: ContentSize,
40    // How to scale content size relative to base content size when selected
41    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        /// Amount of items being dragged in the stack.
104        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 to another slot
118    Dragged(K, K),
119    // Dragged to open space
120    Dropped(K),
121    // Dropped half of the stack
122    SplitDropped(K),
123    // Dragged half of the stack
124    SplitDragged(K, K),
125    // Clicked while selected
126    Used(K),
127    // {Shift,Ctrl}-clicked
128    Request { slot: K, auto_quantity: bool },
129}
130// Handles interactions with slots
131pub struct SlotManager<S: SumSlot> {
132    state: ManagerState<S>,
133    // Rebuilt every frame
134    slot_ids: Vec<widget::Id>,
135    // Rebuilt every frame
136    slots: Vec<S>,
137    events: Vec<Event<S>>,
138    // Widget id for dragging image
139    drag_id: widget::Id,
140    // Size to display dragged content
141    // Note: could potentially be specialized for each slot if needed
142    drag_img_size: Vec2<f32>,
143    pub mouse_over_slot: Option<S>,
144    // Si prefixes settings
145    use_prefixes: bool,
146    prefix_switch_point: u32,
147    /* TODO(heyzoos) Will be useful for whoever works on rendering the number of items "in
148     * hand".
149     *
150     * drag_amount_id: widget::Id,
151     * drag_amount_shadow_id: widget::Id, */
152
153    /* Asset ID pointing to a font set.
154     * amount_font: font::Id, */
155
156    /* Specifies the size of the font used to display number of items held in
157     * a stack when dragging.
158     * amount_font_size: u32, */
159
160    /* Specifies how much space should be used in the margins of the item
161     * amount relative to the slot.
162     * amount_margins: Vec2<f32>, */
163
164    /* Specifies the color of the text used to display the number of items held
165     * in a stack when dragging.
166     * amount_text_color: Color, */
167}
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        /* TODO(heyzoos) Will be useful for whoever works on rendering the number of items "in
179         * hand". amount_font: font::Id,
180         * amount_margins: Vec2<f32>,
181         * amount_font_size: u32,
182         * amount_text_color: Color, */
183    ) -> 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            // TODO(heyzoos) Will be useful for whoever works on rendering the number of items "in
194            // hand". drag_amount_id: gen.next(),
195            // drag_amount_shadow_id: gen.next(),
196            // amount_font,
197            // amount_font_size,
198            // amount_margins,
199            // amount_text_color,
200            drag_img_size,
201        }
202    }
203
204    pub fn maintain(&mut self, ui: &mut conrod_core::UiCell) -> Vec<Event<S>> {
205        // Clear
206        let slot_ids = core::mem::take(&mut self.slot_ids);
207        let slots = core::mem::take(&mut self.slots);
208
209        // Detect drops by of selected item by clicking in empty space
210        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 dragging and mouse is released check if there is a slot widget under the
224        // mouse
225        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 we are dragging and we right click, drop half the stack
236            // on the ground or into the slot under the cursor. This only
237            // works with open slots or slots containing the same kind of
238            // item.
239
240            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                // Get widget under the mouse
259                if let Some(id) = input.widget_under_mouse {
260                    // If over the window widget drop the contents
261                    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                        // If widget is a slot widget swap with it
265                        let (from, to) = (*slot, slots[idx]);
266                        // Don't drag if it is the same slot
267                        if from != to {
268                            self.events.push(Event::Dragged(from, to));
269                        }
270                    }
271                }
272                // Mouse released stop dragging
273                self.state = ManagerState::Idle;
274            }
275
276            // Draw image of contents being dragged
277            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            // TODO(heyzoos) Will be useful for whoever works on rendering the
284            // number of items "in hand".
285            //
286            // if let Some(drag_amount) = drag_amount {
287            //     Text::new(format!("{}", drag_amount).as_str())
288            //         .parent(self.drag_id)
289            //         .font_id(self.amount_font)
290            //         .font_size(self.amount_font_size)
291            //         .bottom_right_with_margins_on(
292            //             self.drag_id,
293            //             self.amount_margins.x as f64,
294            //             self.amount_margins.y as f64,
295            //         )
296            //         .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
297            //         .set(self.drag_amount_shadow_id, ui);
298            //     Text::new(format!("{}", drag_amount).as_str())
299            //         .parent(self.drag_id)
300            //         .font_id(self.amount_font)
301            //         .font_size(self.amount_font_size)
302            //         .bottom_right_with_margins_on(
303            //             self.drag_id,
304            //             self.amount_margins.x as f64,
305            //             self.amount_margins.y as f64,
306            //         )
307            //         .color(self.amount_text_color)
308            //         .set(self.drag_amount_id, ui);
309            // }
310        }
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        // Add to list of slots
330        self.slot_ids.push(widget);
331        self.slots.push(slot);
332
333        let filled = content_img.is_some();
334        // If the slot is no longer filled deselect it or cancel dragging
335        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        // If this is the selected/dragged widget make sure the slot value is up to date
345        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        // TODO: make more robust wrt multiple events in the same frame (eg event order
357        // may matter) TODO: handle taps as well
358        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                    // Swap
364                    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                        // Was clicked again
371                        ManagerState::Selected(widget, slot)
372                    } else {
373                        // Clicked more than once after swap, use and deselect
374                        self.events.push(Event::Used(slot));
375                        ManagerState::Idle
376                    }
377                } else {
378                    // Clicked widget was already selected
379                    // Deselect and emit use if clicked while selected
380                    self.events.push(Event::Used(slot));
381                    ManagerState::Idle
382                }
383            } else {
384                // No widgets were selected
385                if odd_num_clicks && filled {
386                    ManagerState::Selected(widget, slot)
387                } else {
388                    // Selected and then deselected with one or more clicks
389                    ManagerState::Idle
390                }
391            };
392        }
393
394        // Translate ctrl-clicks to stack-requests and shift-clicks to
395        // individual-requests
396        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        // Use on right click if not dragging
419        if input.clicks().right().next().is_some() {
420            match self.state {
421                ManagerState::Selected(_, _) | ManagerState::Idle => {
422                    self.events.push(Event::Used(slot));
423                    // If something is selected, deselect
424                    self.state = ManagerState::Idle;
425                },
426                ManagerState::Dragging(_, _, _, _) => {},
427            }
428        }
429
430        // If not dragging and there is a drag event on this slot start dragging
431        if input.drags().left().next().is_some()
432            && !matches!(self.state, ManagerState::Dragging(_, _, _, _))
433        {
434            // Start dragging if widget is filled
435            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        // Determine whether this slot is being interacted with
443        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    /// Returns Some(slot) if a slot is selected
451    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    /// Sets the SlotManager into an idle state
460    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    // Images for slot background and frame
468    empty_slot: image::Id,
469    selected_slot: image::Id,
470    background_color: Option<Color>,
471
472    // Size of content image
473    content_size: Vec2<f32>,
474    selected_content_scale: f32,
475
476    icon: Option<(image::Id, Vec2<f32>, Option<Color>)>,
477
478    // Amount styling
479    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    // Should we just pass in the ImageKey?
487    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    // Note: icon, amount, and amount_bg are not always used. Is there any cost to having them?
498    struct Ids {
499        background,
500        icon,
501        amount,
502        amount_bg,
503        content,
504    }
505}
506
507/// Represents the state of the Slot widget.
508pub 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    /// Update the state of the Slot.
596    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        // If the key changed update the cached image id
623        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        // Get image ids
636        let content_images = state.cached_images.as_ref().map(|c| c.1.clone());
637        // Get whether this slot is selected
638        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        // No content if it is being dragged
648        let content_images = if let Interaction::Dragging = interaction {
649            None
650        } else {
651            content_images
652        };
653        // Go back to getting image ids
654        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        // Get amount (None => no amount text)
663        let amount = if let Interaction::Dragging = interaction {
664            None // Don't show amount if being dragged
665        } else {
666            slot_key.amount(content_source)
667        };
668
669        // Get slot widget dimensions and position
670        let (x, y, w, h) = rect.x_y_w_h();
671
672        // Draw slot frame/background
673        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        // Draw icon (only when there is not content)
682        // Note: this could potentially be done by the user instead
683        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        // Draw contents
695        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        // Draw amount
713        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 shadow
735            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}