2020-04-04 05:40:00 +00:00
|
|
|
//! A widget for selecting a single value along some linear range.
|
2021-02-16 01:05:54 +00:00
|
|
|
use crate::hud::animate_by_pulse;
|
2020-04-04 05:40:00 +00:00
|
|
|
use conrod_core::{
|
|
|
|
builder_methods, image,
|
2020-04-06 06:15:22 +00:00
|
|
|
input::state::mouse,
|
2020-04-04 05:40:00 +00:00
|
|
|
text::font,
|
|
|
|
widget::{self, Image, Text},
|
|
|
|
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
|
|
|
|
};
|
|
|
|
use vek::*;
|
|
|
|
|
|
|
|
const AMOUNT_SHADOW_OFFSET: [f64; 2] = [1.0, 1.0];
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
pub trait SlotKey<C, I>: Copy {
|
2020-04-04 05:40:00 +00:00
|
|
|
type ImageKey: PartialEq + Send + 'static;
|
|
|
|
/// Returns an Option since the slot could be empty
|
2020-04-11 06:33:06 +00:00
|
|
|
fn image_key(&self, source: &C) -> Option<(Self::ImageKey, Option<Color>)>;
|
2020-04-06 15:25:45 +00:00
|
|
|
fn amount(&self, source: &C) -> Option<u32>;
|
2021-02-16 01:05:54 +00:00
|
|
|
fn image_ids(key: &Self::ImageKey, source: &I) -> Vec<image::Id>;
|
2020-04-04 05:40:00 +00:00
|
|
|
}
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
pub trait SumSlot: Sized + PartialEq + Copy + Send + 'static {}
|
2020-04-04 05:40:00 +00:00
|
|
|
|
2020-04-06 00:03:59 +00:00
|
|
|
pub struct ContentSize {
|
|
|
|
// Width divided by height
|
|
|
|
pub width_height_ratio: f32,
|
|
|
|
// Max fraction of slot widget size that each side can be
|
|
|
|
pub max_fraction: f32,
|
|
|
|
}
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
pub struct SlotMaker<'a, C, I, S: SumSlot> {
|
2020-04-05 19:40:05 +00:00
|
|
|
pub empty_slot: image::Id,
|
|
|
|
pub filled_slot: image::Id,
|
|
|
|
pub selected_slot: image::Id,
|
|
|
|
// Is this useful?
|
2020-04-04 05:40:00 +00:00
|
|
|
pub background_color: Option<Color>,
|
2020-04-06 00:03:59 +00:00
|
|
|
pub content_size: ContentSize,
|
|
|
|
// How to scale content size relative to base content size when selected
|
|
|
|
pub selected_content_scale: f32,
|
2020-04-04 05:40:00 +00:00
|
|
|
pub amount_font: font::Id,
|
|
|
|
pub amount_font_size: u32,
|
|
|
|
pub amount_margins: Vec2<f32>,
|
|
|
|
pub amount_text_color: Color,
|
2020-04-06 15:25:45 +00:00
|
|
|
pub content_source: &'a C,
|
|
|
|
pub image_source: &'a I,
|
|
|
|
pub slot_manager: Option<&'a mut SlotManager<S>>,
|
2021-02-16 01:05:54 +00:00
|
|
|
pub pulse: f32,
|
2020-04-04 05:40:00 +00:00
|
|
|
}
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
impl<'a, C, I, S> SlotMaker<'a, C, I, S>
|
2020-04-04 05:40:00 +00:00
|
|
|
where
|
2020-04-06 15:25:45 +00:00
|
|
|
S: SumSlot,
|
2020-04-04 05:40:00 +00:00
|
|
|
{
|
2020-04-06 15:25:45 +00:00
|
|
|
pub fn fabricate<K: SlotKey<C, I> + Into<S>>(
|
|
|
|
&mut self,
|
|
|
|
contents: K,
|
|
|
|
wh: [f32; 2],
|
|
|
|
) -> Slot<K, C, I, S> {
|
2020-04-06 00:03:59 +00:00
|
|
|
let content_size = {
|
|
|
|
let ContentSize {
|
|
|
|
max_fraction,
|
|
|
|
width_height_ratio,
|
|
|
|
} = self.content_size;
|
|
|
|
let w_max = max_fraction * wh[0];
|
|
|
|
let h_max = max_fraction * wh[1];
|
|
|
|
let max_ratio = w_max / h_max;
|
|
|
|
let (w, h) = if max_ratio > width_height_ratio {
|
|
|
|
(width_height_ratio * h_max, w_max)
|
|
|
|
} else {
|
|
|
|
(w_max, w_max / width_height_ratio)
|
|
|
|
};
|
|
|
|
Vec2::new(w, h)
|
|
|
|
};
|
2020-04-04 05:40:00 +00:00
|
|
|
Slot::new(
|
|
|
|
contents,
|
2020-04-05 19:40:05 +00:00
|
|
|
self.empty_slot,
|
|
|
|
self.selected_slot,
|
2020-10-07 02:23:20 +00:00
|
|
|
self.filled_slot,
|
2020-04-06 00:03:59 +00:00
|
|
|
content_size,
|
|
|
|
self.selected_content_scale,
|
2020-04-04 05:40:00 +00:00
|
|
|
self.amount_font,
|
|
|
|
self.amount_font_size,
|
|
|
|
self.amount_margins,
|
|
|
|
self.amount_text_color,
|
|
|
|
self.content_source,
|
|
|
|
self.image_source,
|
2021-02-16 01:05:54 +00:00
|
|
|
self.pulse,
|
2020-04-04 05:40:00 +00:00
|
|
|
)
|
2020-04-06 00:03:59 +00:00
|
|
|
.wh([wh[0] as f64, wh[1] as f64])
|
2020-04-04 05:40:00 +00:00
|
|
|
.and_then(self.background_color, |s, c| s.with_background_color(c))
|
|
|
|
.and_then(self.slot_manager.as_mut(), |s, m| s.with_manager(m))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
enum ManagerState<K> {
|
2020-04-06 06:15:22 +00:00
|
|
|
Dragging(widget::Id, K, image::Id),
|
2020-04-04 05:40:00 +00:00
|
|
|
Selected(widget::Id, K),
|
|
|
|
Idle,
|
|
|
|
}
|
|
|
|
|
|
|
|
enum Interaction {
|
|
|
|
Selected,
|
|
|
|
Dragging,
|
|
|
|
None,
|
|
|
|
}
|
|
|
|
|
2020-04-04 07:13:51 +00:00
|
|
|
pub enum Event<K> {
|
2020-04-04 05:40:00 +00:00
|
|
|
// Dragged to another slot
|
|
|
|
Dragged(K, K),
|
|
|
|
// Dragged to open space
|
|
|
|
Dropped(K),
|
|
|
|
// Clicked while selected
|
|
|
|
Used(K),
|
|
|
|
}
|
|
|
|
// Handles interactions with slots
|
2020-04-06 15:25:45 +00:00
|
|
|
pub struct SlotManager<S: SumSlot> {
|
|
|
|
state: ManagerState<S>,
|
2020-04-06 06:15:22 +00:00
|
|
|
// Rebuilt every frame
|
|
|
|
slot_ids: Vec<widget::Id>,
|
|
|
|
// Rebuilt every frame
|
2020-04-06 15:25:45 +00:00
|
|
|
slots: Vec<S>,
|
|
|
|
events: Vec<Event<S>>,
|
2020-04-06 06:15:22 +00:00
|
|
|
// Widget id for dragging image
|
|
|
|
drag_id: widget::Id,
|
|
|
|
// Size to display dragged content
|
|
|
|
// Note: could potentially be specialized for each slot if needed
|
|
|
|
drag_img_size: Vec2<f32>,
|
2021-01-08 19:12:09 +00:00
|
|
|
pub mouse_over_slot: Option<S>,
|
2020-04-04 05:40:00 +00:00
|
|
|
}
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
impl<S> SlotManager<S>
|
2020-04-04 05:40:00 +00:00
|
|
|
where
|
2020-04-06 15:25:45 +00:00
|
|
|
S: SumSlot,
|
2020-04-04 05:40:00 +00:00
|
|
|
{
|
2020-04-06 06:15:22 +00:00
|
|
|
pub fn new(mut gen: widget::id::Generator, drag_img_size: Vec2<f32>) -> Self {
|
2020-04-04 07:13:51 +00:00
|
|
|
Self {
|
|
|
|
state: ManagerState::Idle,
|
2020-04-06 06:15:22 +00:00
|
|
|
slot_ids: Vec::new(),
|
2020-04-06 15:25:45 +00:00
|
|
|
slots: Vec::new(),
|
2020-04-04 07:13:51 +00:00
|
|
|
events: Vec::new(),
|
2020-04-06 06:15:22 +00:00
|
|
|
drag_id: gen.next(),
|
|
|
|
drag_img_size,
|
2021-01-08 19:12:09 +00:00
|
|
|
mouse_over_slot: None,
|
2020-04-04 07:13:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
pub fn maintain(&mut self, ui: &mut conrod_core::UiCell) -> Vec<Event<S>> {
|
2020-04-06 06:15:22 +00:00
|
|
|
// Clear
|
|
|
|
let slot_ids = std::mem::replace(&mut self.slot_ids, Vec::new());
|
2020-04-06 15:25:45 +00:00
|
|
|
let slots = std::mem::replace(&mut self.slots, Vec::new());
|
2020-04-06 06:15:22 +00:00
|
|
|
|
2020-04-04 07:13:51 +00:00
|
|
|
// Detect drops by of selected item by clicking in empty space
|
|
|
|
if let ManagerState::Selected(_, slot) = self.state {
|
|
|
|
if ui.widget_input(ui.window).clicks().left().next().is_some() {
|
|
|
|
self.state = ManagerState::Idle;
|
2020-04-06 06:15:22 +00:00
|
|
|
self.events.push(Event::Dropped(slot));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
let input = &ui.global_input().current;
|
|
|
|
self.mouse_over_slot = input
|
|
|
|
.widget_under_mouse
|
|
|
|
.and_then(|x| slot_ids.iter().position(|slot_id| *slot_id == x))
|
|
|
|
.map(|x| slots[x]);
|
|
|
|
|
2020-04-10 02:36:35 +00:00
|
|
|
// If dragging and mouse is released check if there is a slot widget under the
|
2020-04-06 06:15:22 +00:00
|
|
|
// mouse
|
2020-04-10 02:36:35 +00:00
|
|
|
if let ManagerState::Dragging(_, slot, content_img) = &self.state {
|
2020-04-06 06:15:22 +00:00
|
|
|
let content_img = *content_img;
|
|
|
|
if let mouse::ButtonPosition::Up = input.mouse.buttons.left() {
|
|
|
|
// Get widget under the mouse
|
|
|
|
if let Some(id) = input.widget_under_mouse {
|
|
|
|
// If over the window widget drop the contents
|
|
|
|
if id == ui.window {
|
|
|
|
self.events.push(Event::Dropped(*slot));
|
|
|
|
} else if let Some(idx) = slot_ids.iter().position(|slot_id| *slot_id == id) {
|
|
|
|
// If widget is a slot widget swap with it
|
2020-04-24 04:50:16 +00:00
|
|
|
let (from, to) = (*slot, slots[idx]);
|
|
|
|
// Don't drag if it is the same slot
|
|
|
|
if from != to {
|
|
|
|
self.events.push(Event::Dragged(from, to));
|
|
|
|
}
|
2020-04-06 06:15:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Mouse released stop dragging
|
|
|
|
self.state = ManagerState::Idle;
|
2020-04-04 07:13:51 +00:00
|
|
|
}
|
2020-04-06 06:15:22 +00:00
|
|
|
// Draw image of contents being dragged
|
|
|
|
let [mouse_x, mouse_y] = input.mouse.xy;
|
|
|
|
let size = self.drag_img_size.map(|e| e as f64).into_array();
|
|
|
|
super::ghost_image::GhostImage::new(content_img)
|
|
|
|
.wh(size)
|
|
|
|
.xy([mouse_x, mouse_y])
|
|
|
|
.set(self.drag_id, ui);
|
2020-04-04 07:13:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::mem::replace(&mut self.events, Vec::new())
|
|
|
|
}
|
|
|
|
|
2020-04-06 00:03:59 +00:00
|
|
|
fn update(
|
|
|
|
&mut self,
|
|
|
|
widget: widget::Id,
|
2020-04-06 15:25:45 +00:00
|
|
|
slot: S,
|
2020-04-06 00:03:59 +00:00
|
|
|
ui: &conrod_core::Ui,
|
2021-02-16 01:05:54 +00:00
|
|
|
content_img: Option<Vec<image::Id>>,
|
2020-04-06 00:03:59 +00:00
|
|
|
) -> Interaction {
|
2020-04-06 06:15:22 +00:00
|
|
|
// Add to list of slots
|
|
|
|
self.slot_ids.push(widget);
|
2020-04-06 15:25:45 +00:00
|
|
|
self.slots.push(slot);
|
2020-04-06 06:15:22 +00:00
|
|
|
|
|
|
|
let filled = content_img.is_some();
|
2020-04-06 00:03:59 +00:00
|
|
|
// If the slot is no longer filled deselect it or cancel dragging
|
|
|
|
match &self.state {
|
2020-04-06 06:15:22 +00:00
|
|
|
ManagerState::Selected(id, _) | ManagerState::Dragging(id, _, _)
|
2020-04-06 00:03:59 +00:00
|
|
|
if *id == widget && !filled =>
|
|
|
|
{
|
|
|
|
self.state = ManagerState::Idle;
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
2020-04-04 05:40:00 +00:00
|
|
|
// If this is the selected/dragged widget make sure the slot value is up to date
|
|
|
|
match &mut self.state {
|
2020-04-06 06:15:22 +00:00
|
|
|
ManagerState::Selected(id, stored_slot)
|
|
|
|
| ManagerState::Dragging(id, stored_slot, _)
|
2020-04-04 05:40:00 +00:00
|
|
|
if *id == widget =>
|
|
|
|
{
|
|
|
|
*stored_slot = slot
|
|
|
|
},
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
2020-04-10 04:33:35 +00:00
|
|
|
let input = ui.widget_input(widget);
|
2020-04-04 05:40:00 +00:00
|
|
|
// TODO: make more robust wrt multiple events in the same frame (eg event order
|
|
|
|
// may matter) TODO: handle taps as well
|
2020-04-10 04:33:35 +00:00
|
|
|
let click_count = input.clicks().left().count();
|
2020-04-04 05:40:00 +00:00
|
|
|
if click_count > 0 {
|
|
|
|
let odd_num_clicks = click_count % 2 == 1;
|
|
|
|
self.state = if let ManagerState::Selected(id, other_slot) = self.state {
|
|
|
|
if id != widget {
|
|
|
|
// Swap
|
|
|
|
if slot != other_slot {
|
|
|
|
self.events.push(Event::Dragged(other_slot, slot));
|
|
|
|
}
|
|
|
|
if click_count == 1 {
|
|
|
|
ManagerState::Idle
|
|
|
|
} else if click_count == 2 {
|
|
|
|
// Was clicked again
|
|
|
|
ManagerState::Selected(widget, slot)
|
|
|
|
} else {
|
|
|
|
// Clicked more than once after swap, use and deselect
|
|
|
|
self.events.push(Event::Used(slot));
|
|
|
|
ManagerState::Idle
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Clicked widget was already selected
|
|
|
|
// Deselect and emit use if clicked while selected
|
|
|
|
self.events.push(Event::Used(slot));
|
|
|
|
ManagerState::Idle
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// No widgets were selected
|
2020-04-06 00:03:59 +00:00
|
|
|
if odd_num_clicks && filled {
|
2020-04-04 05:40:00 +00:00
|
|
|
ManagerState::Selected(widget, slot)
|
|
|
|
} else {
|
|
|
|
// Selected and then deselected with one or more clicks
|
|
|
|
ManagerState::Idle
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-04-10 04:33:35 +00:00
|
|
|
// Use on right click if not dragging
|
|
|
|
if input.clicks().right().next().is_some() {
|
2020-04-06 06:15:22 +00:00
|
|
|
match self.state {
|
2020-04-10 02:59:28 +00:00
|
|
|
ManagerState::Selected(_, _) | ManagerState::Idle => {
|
2020-04-10 04:33:35 +00:00
|
|
|
self.events.push(Event::Used(slot));
|
|
|
|
// If something is selected, deselect
|
|
|
|
self.state = ManagerState::Idle;
|
2020-04-06 06:15:22 +00:00
|
|
|
},
|
2020-04-10 04:33:35 +00:00
|
|
|
ManagerState::Dragging(_, _, _) => {},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If not dragging and there is a drag event on this slot start dragging
|
|
|
|
if input.drags().left().next().is_some()
|
|
|
|
&& !matches!(self.state, ManagerState::Dragging(_, _, _))
|
|
|
|
{
|
|
|
|
// Start dragging if widget is filled
|
|
|
|
if let Some(img) = content_img {
|
2021-02-16 01:05:54 +00:00
|
|
|
self.state = ManagerState::Dragging(widget, slot, img[0]);
|
2020-04-06 06:15:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-04 05:40:00 +00:00
|
|
|
// Determine whether this slot is being interacted with
|
|
|
|
match self.state {
|
|
|
|
ManagerState::Selected(id, _) if id == widget => Interaction::Selected,
|
2020-04-06 06:15:22 +00:00
|
|
|
ManagerState::Dragging(id, _, _) if id == widget => Interaction::Dragging,
|
2020-04-04 05:40:00 +00:00
|
|
|
_ => Interaction::None,
|
|
|
|
}
|
|
|
|
}
|
2020-04-11 06:33:06 +00:00
|
|
|
|
|
|
|
/// Returns Some(slot) if a slot is selected
|
|
|
|
pub fn selected(&self) -> Option<S> {
|
|
|
|
if let ManagerState::Selected(_, s) = self.state {
|
|
|
|
Some(s)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Sets the SlotManager into an idle state
|
|
|
|
pub fn idle(&mut self) { self.state = ManagerState::Idle; }
|
2020-04-04 05:40:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(WidgetCommon)]
|
2020-04-06 15:25:45 +00:00
|
|
|
pub struct Slot<'a, K: SlotKey<C, I> + Into<S>, C, I, S: SumSlot> {
|
|
|
|
slot_key: K,
|
2020-04-04 05:40:00 +00:00
|
|
|
|
2020-04-05 19:40:05 +00:00
|
|
|
// Images for slot background and frame
|
|
|
|
empty_slot: image::Id,
|
|
|
|
selected_slot: image::Id,
|
2020-04-04 05:40:00 +00:00
|
|
|
background_color: Option<Color>,
|
|
|
|
|
|
|
|
// Size of content image
|
|
|
|
content_size: Vec2<f32>,
|
2020-04-06 00:03:59 +00:00
|
|
|
selected_content_scale: f32,
|
2020-04-04 05:40:00 +00:00
|
|
|
|
2020-04-04 17:51:41 +00:00
|
|
|
icon: Option<(image::Id, Vec2<f32>, Option<Color>)>,
|
|
|
|
|
2020-04-04 05:40:00 +00:00
|
|
|
// Amount styling
|
|
|
|
amount_font: font::Id,
|
|
|
|
amount_font_size: u32,
|
|
|
|
amount_margins: Vec2<f32>,
|
|
|
|
amount_text_color: Color,
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
slot_manager: Option<&'a mut SlotManager<S>>,
|
2020-10-07 02:23:20 +00:00
|
|
|
filled_slot: image::Id,
|
2020-04-04 05:40:00 +00:00
|
|
|
// Should we just pass in the ImageKey?
|
2020-04-06 15:25:45 +00:00
|
|
|
content_source: &'a C,
|
|
|
|
image_source: &'a I,
|
2020-04-04 05:40:00 +00:00
|
|
|
|
2021-02-16 01:05:54 +00:00
|
|
|
pulse: f32,
|
|
|
|
|
2020-04-04 05:40:00 +00:00
|
|
|
#[conrod(common_builder)]
|
|
|
|
common: widget::CommonBuilder,
|
|
|
|
}
|
|
|
|
|
|
|
|
widget_ids! {
|
|
|
|
// Note: icon, amount, and amount_bg are not always used. Is there any cost to having them?
|
|
|
|
struct Ids {
|
|
|
|
background,
|
|
|
|
icon,
|
|
|
|
amount,
|
|
|
|
amount_bg,
|
|
|
|
content,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Represents the state of the Slot widget.
|
2020-04-06 15:25:45 +00:00
|
|
|
pub struct State<K> {
|
2020-04-04 05:40:00 +00:00
|
|
|
ids: Ids,
|
2021-02-16 01:05:54 +00:00
|
|
|
cached_images: Option<(K, Vec<image::Id>)>,
|
2020-04-04 05:40:00 +00:00
|
|
|
}
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
impl<'a, K, C, I, S> Slot<'a, K, C, I, S>
|
2020-04-04 05:40:00 +00:00
|
|
|
where
|
2020-04-06 15:25:45 +00:00
|
|
|
K: SlotKey<C, I> + Into<S>,
|
|
|
|
S: SumSlot,
|
2020-04-04 05:40:00 +00:00
|
|
|
{
|
|
|
|
builder_methods! {
|
|
|
|
pub with_background_color { background_color = Some(Color) }
|
|
|
|
}
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
pub fn with_manager(mut self, slot_manager: &'a mut SlotManager<S>) -> Self {
|
|
|
|
self.slot_manager = Some(slot_manager);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2020-10-07 02:23:20 +00:00
|
|
|
pub fn filled_slot(mut self, img: image::Id) -> Self {
|
|
|
|
self.filled_slot = img;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2020-04-04 17:51:41 +00:00
|
|
|
pub fn with_icon(mut self, img: image::Id, size: Vec2<f32>, color: Option<Color>) -> Self {
|
|
|
|
self.icon = Some((img, size, color));
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2020-06-10 19:47:36 +00:00
|
|
|
#[allow(clippy::too_many_arguments)] // TODO: Pending review in #587
|
2020-04-04 05:40:00 +00:00
|
|
|
fn new(
|
2020-04-06 15:25:45 +00:00
|
|
|
slot_key: K,
|
2020-04-05 19:40:05 +00:00
|
|
|
empty_slot: image::Id,
|
|
|
|
filled_slot: image::Id,
|
|
|
|
selected_slot: image::Id,
|
2020-04-04 05:40:00 +00:00
|
|
|
content_size: Vec2<f32>,
|
2020-04-06 00:03:59 +00:00
|
|
|
selected_content_scale: f32,
|
2020-04-04 05:40:00 +00:00
|
|
|
amount_font: font::Id,
|
|
|
|
amount_font_size: u32,
|
|
|
|
amount_margins: Vec2<f32>,
|
|
|
|
amount_text_color: Color,
|
2020-04-06 15:25:45 +00:00
|
|
|
content_source: &'a C,
|
|
|
|
image_source: &'a I,
|
2021-02-16 01:05:54 +00:00
|
|
|
pulse: f32,
|
2020-04-04 05:40:00 +00:00
|
|
|
) -> Self {
|
|
|
|
Self {
|
2020-04-06 15:25:45 +00:00
|
|
|
slot_key,
|
2020-04-05 19:40:05 +00:00
|
|
|
empty_slot,
|
|
|
|
filled_slot,
|
|
|
|
selected_slot,
|
2020-04-04 05:40:00 +00:00
|
|
|
background_color: None,
|
|
|
|
content_size,
|
2020-04-06 00:03:59 +00:00
|
|
|
selected_content_scale,
|
2020-04-04 17:51:41 +00:00
|
|
|
icon: None,
|
2020-04-04 05:40:00 +00:00
|
|
|
amount_font,
|
|
|
|
amount_font_size,
|
|
|
|
amount_margins,
|
|
|
|
amount_text_color,
|
|
|
|
slot_manager: None,
|
|
|
|
content_source,
|
|
|
|
image_source,
|
2021-02-16 01:05:54 +00:00
|
|
|
pulse,
|
2020-04-04 05:40:00 +00:00
|
|
|
common: widget::CommonBuilder::default(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-06 15:25:45 +00:00
|
|
|
impl<'a, K, C, I, S> Widget for Slot<'a, K, C, I, S>
|
2020-04-04 05:40:00 +00:00
|
|
|
where
|
2020-04-06 15:25:45 +00:00
|
|
|
K: SlotKey<C, I> + Into<S>,
|
|
|
|
S: SumSlot,
|
2020-04-04 05:40:00 +00:00
|
|
|
{
|
|
|
|
type Event = ();
|
2020-04-06 15:25:45 +00:00
|
|
|
type State = State<K::ImageKey>;
|
2020-04-04 05:40:00 +00:00
|
|
|
type Style = ();
|
|
|
|
|
|
|
|
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
|
|
|
|
State {
|
|
|
|
ids: Ids::new(id_gen),
|
2021-02-16 01:05:54 +00:00
|
|
|
cached_images: None,
|
2020-04-04 05:40:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-10 19:47:36 +00:00
|
|
|
#[allow(clippy::unused_unit)] // TODO: Pending review in #587
|
2020-04-04 05:40:00 +00:00
|
|
|
fn style(&self) -> Self::Style { () }
|
|
|
|
|
2021-01-08 19:12:09 +00:00
|
|
|
/// Update the state of the Slot.
|
2020-04-04 05:40:00 +00:00
|
|
|
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
|
|
|
|
let widget::UpdateArgs {
|
|
|
|
id,
|
|
|
|
state,
|
|
|
|
rect,
|
|
|
|
ui,
|
|
|
|
..
|
|
|
|
} = args;
|
2021-01-08 19:12:09 +00:00
|
|
|
|
2020-04-04 05:40:00 +00:00
|
|
|
let Slot {
|
2020-04-06 15:25:45 +00:00
|
|
|
slot_key,
|
2020-04-05 19:40:05 +00:00
|
|
|
empty_slot,
|
|
|
|
selected_slot,
|
2020-04-04 05:40:00 +00:00
|
|
|
background_color,
|
|
|
|
content_size,
|
2020-04-06 00:03:59 +00:00
|
|
|
selected_content_scale,
|
2020-04-04 17:51:41 +00:00
|
|
|
icon,
|
2020-04-04 05:40:00 +00:00
|
|
|
amount_font,
|
|
|
|
amount_font_size,
|
|
|
|
amount_margins,
|
|
|
|
amount_text_color,
|
|
|
|
content_source,
|
|
|
|
image_source,
|
|
|
|
..
|
|
|
|
} = self;
|
|
|
|
|
|
|
|
// If the key changed update the cached image id
|
2020-04-11 06:33:06 +00:00
|
|
|
let (image_key, content_color) = slot_key
|
|
|
|
.image_key(content_source)
|
|
|
|
.map_or((None, None), |(i, c)| (Some(i), c));
|
2021-02-16 01:05:54 +00:00
|
|
|
if state.cached_images.as_ref().map(|c| &c.0) != image_key.as_ref() {
|
2020-04-04 05:40:00 +00:00
|
|
|
state.update(|state| {
|
2021-02-16 01:05:54 +00:00
|
|
|
state.cached_images = image_key.map(|key| {
|
|
|
|
let image_ids = K::image_ids(&key, &image_source);
|
|
|
|
(key, image_ids)
|
2020-04-04 05:40:00 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get image ids
|
2021-02-16 01:05:54 +00:00
|
|
|
let content_images = state.cached_images.as_ref().map(|c| c.1.clone());
|
2020-04-06 06:15:22 +00:00
|
|
|
// Get whether this slot is selected
|
|
|
|
let interaction = self.slot_manager.map_or(Interaction::None, |m| {
|
2021-02-16 01:05:54 +00:00
|
|
|
m.update(id, slot_key.into(), ui, content_images.clone())
|
2020-04-06 06:15:22 +00:00
|
|
|
});
|
|
|
|
// No content if it is being dragged
|
2021-02-16 01:05:54 +00:00
|
|
|
let content_images = if let Interaction::Dragging = interaction {
|
2020-04-06 06:15:22 +00:00
|
|
|
None
|
|
|
|
} else {
|
2021-02-16 01:05:54 +00:00
|
|
|
content_images
|
2020-04-06 06:15:22 +00:00
|
|
|
};
|
|
|
|
// Go back to getting image ids
|
2020-04-05 19:40:05 +00:00
|
|
|
let slot_image = if let Interaction::Selected = interaction {
|
|
|
|
selected_slot
|
2021-02-16 01:05:54 +00:00
|
|
|
} else if content_images.is_some() {
|
2020-10-07 02:23:20 +00:00
|
|
|
self.filled_slot
|
2020-04-04 05:40:00 +00:00
|
|
|
} else {
|
2020-04-05 19:40:05 +00:00
|
|
|
empty_slot
|
2020-04-04 05:40:00 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Get amount (None => no amount text)
|
2020-04-10 02:59:28 +00:00
|
|
|
let amount = if let Interaction::Dragging = interaction {
|
|
|
|
None // Don't show amount if being dragged
|
|
|
|
} else {
|
|
|
|
slot_key.amount(content_source)
|
|
|
|
};
|
2020-04-04 05:40:00 +00:00
|
|
|
|
|
|
|
// Get slot widget dimensions and position
|
|
|
|
let (x, y, w, h) = rect.x_y_w_h();
|
|
|
|
|
2020-04-05 19:40:05 +00:00
|
|
|
// Draw slot frame/background
|
|
|
|
Image::new(slot_image)
|
2020-04-04 05:40:00 +00:00
|
|
|
.x_y(x, y)
|
|
|
|
.w_h(w, h)
|
|
|
|
.parent(id)
|
|
|
|
.graphics_for(id)
|
|
|
|
.color(background_color)
|
|
|
|
.set(state.ids.background, ui);
|
|
|
|
|
2020-04-05 19:40:05 +00:00
|
|
|
// Draw icon (only when there is not content)
|
|
|
|
// Note: this could potentially be done by the user instead
|
2021-02-16 01:05:54 +00:00
|
|
|
if let (Some((icon_image, size, color)), true) = (icon, content_images.is_none()) {
|
2020-04-04 17:51:41 +00:00
|
|
|
let wh = size.map(|e| e as f64).into_array();
|
2020-04-04 05:40:00 +00:00
|
|
|
Image::new(icon_image)
|
|
|
|
.x_y(x, y)
|
|
|
|
.wh(wh)
|
|
|
|
.parent(id)
|
|
|
|
.graphics_for(id)
|
2020-04-04 17:51:41 +00:00
|
|
|
.color(color)
|
2020-04-04 05:40:00 +00:00
|
|
|
.set(state.ids.icon, ui);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw contents
|
2021-02-16 01:05:54 +00:00
|
|
|
if let Some(content_images) = content_images {
|
|
|
|
Image::new(animate_by_pulse(&content_images, self.pulse))
|
2020-04-04 05:40:00 +00:00
|
|
|
.x_y(x, y)
|
2020-04-06 00:03:59 +00:00
|
|
|
.wh((content_size
|
|
|
|
* if let Interaction::Selected = interaction {
|
|
|
|
selected_content_scale
|
|
|
|
} else {
|
|
|
|
1.0
|
|
|
|
})
|
2020-04-04 05:40:00 +00:00
|
|
|
.map(|e| e as f64)
|
|
|
|
.into_array())
|
2020-04-11 06:33:06 +00:00
|
|
|
.color(content_color)
|
2020-04-04 05:40:00 +00:00
|
|
|
.parent(id)
|
|
|
|
.graphics_for(id)
|
2020-04-04 07:13:51 +00:00
|
|
|
.set(state.ids.content, ui);
|
2020-04-04 05:40:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Draw amount
|
|
|
|
if let Some(amount) = amount {
|
|
|
|
let amount = format!("{}", &amount);
|
|
|
|
// Text shadow
|
|
|
|
Text::new(&amount)
|
|
|
|
.font_id(amount_font)
|
|
|
|
.font_size(amount_font_size)
|
2020-04-12 01:51:36 +00:00
|
|
|
.bottom_right_with_margins_on(
|
2020-04-04 07:13:51 +00:00
|
|
|
state.ids.content,
|
|
|
|
amount_margins.x as f64,
|
|
|
|
amount_margins.y as f64,
|
|
|
|
)
|
|
|
|
.parent(id)
|
|
|
|
.graphics_for(id)
|
2020-04-04 05:40:00 +00:00
|
|
|
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
|
|
|
|
.set(state.ids.amount_bg, ui);
|
|
|
|
Text::new(&amount)
|
|
|
|
.parent(id)
|
|
|
|
.graphics_for(id)
|
|
|
|
.bottom_left_with_margins_on(
|
|
|
|
state.ids.amount_bg,
|
|
|
|
AMOUNT_SHADOW_OFFSET[0],
|
|
|
|
AMOUNT_SHADOW_OFFSET[1],
|
|
|
|
)
|
|
|
|
.font_id(amount_font)
|
|
|
|
.font_size(amount_font_size)
|
|
|
|
.color(amount_text_color)
|
|
|
|
.set(state.ids.amount, ui);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|