use crate::message::PlotUiMessage;
use iced::{Element, keyboard, mouse, widget};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct PlotControls {
drag: HashMap<mouse::Button, DragAction>,
scroll: HashMap<keyboard::Modifiers, ScrollAction>,
click: HashMap<mouse::Button, ClickAction>,
double_click: HashMap<mouse::Button, ClickAction>,
key: HashMap<keyboard::Key, KeyAction>,
drag_delta_threshold: f32,
selection_padding: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DragAction {
Pan,
BoxZoom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollAction {
Pan,
Zoom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClickAction {
Pick,
Autoscale,
ClearPick,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KeyAction {
Autoscale,
ClearPick,
PanBy {
direction: PanDirection,
fraction: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PanDirection {
Left,
Right,
Up,
Down,
}
impl Default for PlotControls {
fn default() -> Self {
let mut controls = Self {
drag: HashMap::new(),
scroll: HashMap::new(),
click: HashMap::new(),
double_click: HashMap::new(),
key: HashMap::new(),
drag_delta_threshold: 4.0,
selection_padding: 0.02,
};
controls
.bind_drag(mouse::Button::Left, DragAction::Pan)
.bind_drag(mouse::Button::Right, DragAction::BoxZoom)
.bind_scroll(keyboard::Modifiers::NONE, ScrollAction::Pan)
.bind_scroll(keyboard::Modifiers::CTRL, ScrollAction::Zoom)
.bind_click(mouse::Button::Left, ClickAction::Pick)
.bind_double_click(mouse::Button::Left, ClickAction::Autoscale)
.bind_key(
keyboard::Key::Named(keyboard::key::Named::Escape),
KeyAction::ClearPick,
);
controls.bind_arrow_pan(0.1);
controls
}
}
impl PlotControls {
pub fn bind_drag(&mut self, button: mouse::Button, action: DragAction) -> &mut Self {
self.drag.insert(button, action);
self
}
pub fn unbind_drag(&mut self, button: mouse::Button) -> Option<DragAction> {
self.drag.remove(&button)
}
pub fn drag_action(&self, button: mouse::Button) -> Option<DragAction> {
self.drag.get(&button).copied()
}
pub fn drag_is_bound(&self, button: mouse::Button, action: DragAction) -> bool {
self.drag_action(button) == Some(action)
}
pub fn bind_scroll(
&mut self,
modifiers: keyboard::Modifiers,
action: ScrollAction,
) -> &mut Self {
self.scroll.insert(modifiers, action);
self
}
pub fn unbind_scroll(&mut self, modifiers: keyboard::Modifiers) -> Option<ScrollAction> {
self.scroll.remove(&modifiers)
}
pub fn scroll_action(&self, modifiers: keyboard::Modifiers) -> Option<ScrollAction> {
self.scroll.get(&modifiers).copied().or_else(|| {
modifiers
.contains(keyboard::Modifiers::CTRL)
.then(|| self.scroll.get(&keyboard::Modifiers::CTRL).copied())
.flatten()
})
}
pub fn scroll_is_bound(&self, modifiers: keyboard::Modifiers, action: ScrollAction) -> bool {
self.scroll.get(&modifiers).copied() == Some(action)
}
pub fn bind_click(&mut self, button: mouse::Button, action: ClickAction) -> &mut Self {
self.click.insert(button, action);
self
}
pub fn unbind_click(&mut self, button: mouse::Button) -> Option<ClickAction> {
self.click.remove(&button)
}
pub fn click_action(&self, button: mouse::Button) -> Option<ClickAction> {
self.click.get(&button).copied()
}
pub fn click_is_bound(&self, button: mouse::Button, action: ClickAction) -> bool {
self.click_action(button) == Some(action)
}
pub fn bind_double_click(&mut self, button: mouse::Button, action: ClickAction) -> &mut Self {
self.double_click.insert(button, action);
self
}
pub fn unbind_double_click(&mut self, button: mouse::Button) -> Option<ClickAction> {
self.double_click.remove(&button)
}
pub fn double_click_action(&self, button: mouse::Button) -> Option<ClickAction> {
self.double_click.get(&button).copied()
}
pub fn double_click_is_bound(&self, button: mouse::Button, action: ClickAction) -> bool {
self.double_click_action(button) == Some(action)
}
pub fn bind_key(&mut self, key: keyboard::Key, action: KeyAction) -> &mut Self {
self.key.insert(key, action);
self
}
pub fn unbind_key(&mut self, key: &keyboard::Key) -> Option<KeyAction> {
self.key.remove(key)
}
pub fn key_action(&self, key: &keyboard::Key) -> Option<KeyAction> {
self.key.get(key).copied()
}
pub fn key_is_bound(&self, key: &keyboard::Key, action: KeyAction) -> bool {
self.key_action(key) == Some(action)
}
pub fn drag_delta_threshold(&self) -> f32 {
self.drag_delta_threshold
}
pub fn set_drag_delta_threshold(&mut self, threshold: f32) -> &mut Self {
self.drag_delta_threshold = threshold.max(0.0);
self
}
pub fn selection_padding(&self) -> f64 {
self.selection_padding
}
pub fn set_selection_padding(&mut self, padding: f64) -> &mut Self {
self.selection_padding = padding.max(0.0);
self
}
pub fn remove_drag_action(&mut self, action: DragAction) -> &mut Self {
self.drag.retain(|_, bound| *bound != action);
self
}
pub fn set_drag_action(
&mut self,
action: DragAction,
button: Option<mouse::Button>,
) -> &mut Self {
self.remove_drag_action(action);
if let Some(button) = button {
self.bind_drag(button, action);
}
self
}
pub fn remove_key_action(&mut self, action: KeyAction) -> &mut Self {
self.key.retain(|_, bound| *bound != action);
self
}
pub fn set_key_action(&mut self, action: KeyAction, key: Option<keyboard::Key>) -> &mut Self {
self.remove_key_action(action);
if let Some(key) = key {
self.bind_key(key, action);
}
self
}
pub fn bind_arrow_pan(&mut self, fraction: f64) -> &mut Self {
self.bind_key(
keyboard::Key::Named(keyboard::key::Named::ArrowLeft),
KeyAction::PanBy {
direction: PanDirection::Left,
fraction,
},
)
.bind_key(
keyboard::Key::Named(keyboard::key::Named::ArrowRight),
KeyAction::PanBy {
direction: PanDirection::Right,
fraction,
},
)
.bind_key(
keyboard::Key::Named(keyboard::key::Named::ArrowUp),
KeyAction::PanBy {
direction: PanDirection::Up,
fraction,
},
)
.bind_key(
keyboard::Key::Named(keyboard::key::Named::ArrowDown),
KeyAction::PanBy {
direction: PanDirection::Down,
fraction,
},
)
}
pub fn unbind_arrow_pan(&mut self) -> &mut Self {
for key in [
keyboard::key::Named::ArrowLeft,
keyboard::key::Named::ArrowRight,
keyboard::key::Named::ArrowUp,
keyboard::key::Named::ArrowDown,
] {
let key = keyboard::Key::Named(key);
if matches!(self.key_action(&key), Some(KeyAction::PanBy { .. })) {
self.unbind_key(&key);
}
}
self
}
pub fn has_pick_action(&self) -> bool {
self.click
.values()
.any(|action| *action == ClickAction::Pick)
|| self
.double_click
.values()
.any(|action| *action == ClickAction::Pick)
}
pub fn arrows_to_pan_enabled(&self) -> bool {
[
keyboard::key::Named::ArrowLeft,
keyboard::key::Named::ArrowRight,
keyboard::key::Named::ArrowUp,
keyboard::key::Named::ArrowDown,
]
.into_iter()
.all(|key| {
matches!(
self.key_action(&keyboard::Key::Named(key)),
Some(KeyAction::PanBy { .. })
)
})
}
}
impl PlotControls {
pub(crate) fn view_controls_overlay_panel(
&self,
has_legend: bool,
) -> Element<'_, PlotUiMessage> {
let txt = |t| widget::text(t).size(12.0).style(widget::text::base);
let mut content =
widget::column![txt("Controls").style(widget::text::primary)].spacing(2.0);
let mut bindings = self.binding_descriptions();
bindings.sort();
for binding in bindings {
content = content.push(widget::text(binding).size(12.0).style(widget::text::base));
}
if has_legend {
content = content.push(txt("Click icon in legend to toggle visibility."));
}
content.into()
}
}
impl PlotControls {
fn binding_descriptions(&self) -> Vec<String> {
let mut bindings = Vec::new();
bindings.extend(self.drag.iter().map(|(button, action)| {
format!(
"{}-drag: {}",
mouse_button_label(*button),
drag_action_label(*action)
)
}));
bindings.extend(self.scroll.iter().map(|(modifiers, action)| {
let trigger = if modifiers.is_empty() {
"Scroll".to_owned()
} else {
format!("{} + scroll", keyboard_modifiers_label(*modifiers))
};
format!("{trigger}: {}", scroll_action_label(*action))
}));
bindings.extend(self.click.iter().map(|(button, action)| {
format!(
"{}-click: {}",
mouse_button_label(*button),
click_action_label(*action)
)
}));
bindings.extend(self.double_click.iter().map(|(button, action)| {
format!(
"{} double-click: {}",
mouse_button_label(*button),
click_action_label(*action)
)
}));
bindings.extend(self.key.iter().map(|(key, action)| {
format!("{}: {}", keyboard_key_label(key), key_action_label(*action))
}));
bindings
}
}
fn mouse_button_label(button: mouse::Button) -> String {
match button {
mouse::Button::Left => "Left".to_owned(),
mouse::Button::Right => "Right".to_owned(),
mouse::Button::Middle => "Middle".to_owned(),
mouse::Button::Back => "Back".to_owned(),
mouse::Button::Forward => "Forward".to_owned(),
mouse::Button::Other(number) => format!("Mouse {number}"),
}
}
fn keyboard_key_label(key: &keyboard::Key) -> String {
match key {
keyboard::Key::Named(named) => format!("{named:?}"),
keyboard::Key::Character(character) => character.to_string(),
keyboard::Key::Unidentified => "Unidentified".to_owned(),
}
}
fn drag_action_label(action: DragAction) -> &'static str {
match action {
DragAction::Pan => "pan",
DragAction::BoxZoom => "box zoom",
}
}
fn scroll_action_label(action: ScrollAction) -> &'static str {
match action {
ScrollAction::Pan => "pan",
ScrollAction::Zoom => "zoom at cursor",
}
}
fn click_action_label(action: ClickAction) -> &'static str {
match action {
ClickAction::Pick => "pick point",
ClickAction::Autoscale => "reset / autoscale",
ClickAction::ClearPick => "clear picked points",
}
}
fn key_action_label(action: KeyAction) -> String {
match action {
KeyAction::Autoscale => "reset / autoscale".to_owned(),
KeyAction::ClearPick => "clear picked points".to_owned(),
KeyAction::PanBy {
direction,
fraction,
} => format!("pan {direction:?} by {:.0}%", fraction * 100.0),
}
}
fn keyboard_modifiers_label(modifiers: keyboard::Modifiers) -> String {
let mut labels = Vec::new();
if modifiers.contains(keyboard::Modifiers::CTRL) {
labels.push("Ctrl");
}
if modifiers.contains(keyboard::Modifiers::SHIFT) {
labels.push("Shift");
}
if modifiers.contains(keyboard::Modifiers::ALT) {
labels.push("Alt");
}
if modifiers.contains(keyboard::Modifiers::LOGO) {
labels.push("Logo");
}
if labels.is_empty() {
"No modifier".to_owned()
} else {
labels.join(" + ")
}
}