use crate::event::{Event, KeyEvent, MouseButton, MouseEvent, MouseKind};
use crate::geometry::Rect;
use crate::keymap::Key;
use crate::widget::{EventCx, PaintCx, Widget};
use super::super::TabEdit;
use super::super::click::Click;
use super::super::context_item;
use super::super::context_menu::{self, ContextMenu};
use super::super::edge_scroll::{Edge, EdgeScroll, Zone};
use super::super::rows::RowScroll;
use super::super::tab_model::{Direction, drop_target};
use super::drop::{Aim, Spring};
use super::{Flat, Tree, TreeNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeMove {
pub key: String,
pub parent: Option<String>,
pub from: usize,
pub to: usize,
}
impl TreeMove {
pub fn apply<T>(&self, siblings: &mut Vec<T>) {
let mut unused = 0;
TabEdit::Move { from: self.from, to: self.to }.apply(siblings, &mut unused);
}
}
pub(super) struct Arrange<'k> {
pub(super) key: &'k str,
pub(super) parent: Option<&'k str>,
pub(super) order: Option<(usize, usize)>,
}
#[derive(Debug, Clone)]
struct Press {
key: String,
keys: Vec<String>,
start: (i32, i32),
pointer: (i32, i32),
dragging: bool,
reduce: bool,
}
#[derive(Debug, Default)]
struct TreePointer {
pressed: Option<Press>,
edge: EdgeScroll,
}
pub(super) struct Drag {
pub(super) key: String,
pub(super) keys: Vec<String>,
pub(super) pointer: (i32, i32),
}
#[derive(Debug, Default)]
struct MenuNode(Option<String>);
impl<Msg: 'static> Tree<Msg> {
pub(super) fn siblings(&self, key: &str) -> Option<(Option<&str>, &[TreeNode])> {
fn find<'a>(
nodes: &'a [TreeNode],
parent: Option<&'a str>,
key: &str,
) -> Option<(Option<&'a str>, &'a [TreeNode])> {
if nodes.iter().any(|node| node.key == key) {
return Some((parent, nodes));
}
nodes.iter().find_map(|node| find(&node.children, Some(&node.key), key))
}
find(&self.roots, None, key)
}
pub(super) fn move_node(&self, cx: &mut EventCx<'_, Msg>, key: &str, parent: Option<&str>, from: usize, to: usize) {
if from != to
&& let Some(message) = &self.on_move
{
cx.emit(message(TreeMove { key: key.to_owned(), parent: parent.map(str::to_owned), from, to }));
}
}
pub(super) fn move_key(&self, cx: &mut EventCx<'_, Msg>, key: &KeyEvent) -> bool {
let mods = key.chord.mods;
let up = key.chord.key == Key::Up;
if self.on_move.is_none() || !mods.ctrl || !mods.shift || mods.alt || !(up || key.chord.key == Key::Down) {
return false;
}
let Some(selected) = self.selected.as_deref() else {
return true;
};
let Some((parent, siblings)) = self.siblings(selected) else {
return true;
};
let Some(from) = siblings.iter().position(|node| node.key == selected) else {
return true;
};
let to = if up { from.saturating_sub(1) } else { (from + 1).min(siblings.len() - 1) };
self.move_node(cx, selected, parent, from, to);
true
}
pub(super) fn resting(&self, key: &str) -> Vec<Flat<'_>> {
let parent = self.siblings(key).and_then(|(parent, _)| parent);
self.flatten_with(Some(&Arrange { key, parent, order: None }))
}
pub(super) fn landing(&self, key: &str, pointer: (i32, i32), area: Rect, offset: usize) -> Option<(usize, usize)> {
let flat = self.resting(key);
let row = flat.iter().position(|row| row.node.key == key)?;
let parent = flat[row].parent;
let from = flat[row].index;
let visible = usize::from(area.height);
let slots: Vec<(usize, Rect)> = flat
.iter()
.enumerate()
.skip(offset)
.take(visible)
.filter(|(_, other)| other.parent == parent)
.map(|(at, other)| {
let y = area.y + i32::try_from(at - offset).unwrap_or(0);
(other.index, Rect::new(area.x, y, area.width, 1))
})
.collect();
Some((from, drop_target(&slots, from, pointer, Direction::Down)))
}
pub(super) fn drag(&self, cx: &mut PaintCx<'_>) -> Option<Drag> {
if self.on_move.is_none() && self.dropping.is_none() {
return None;
}
let press = cx.memory::<TreePointer>().pressed.clone()?;
(press.dragging && self.siblings(&press.key).is_some()).then_some(Drag {
key: press.key,
keys: press.keys,
pointer: press.pointer,
})
}
pub(super) fn drag_pointer(
&self,
cx: &mut EventCx<'_, Msg>,
mouse: &MouseEvent,
flat: &[Flat<'_>],
index: Option<usize>,
) -> Option<bool> {
let at = (mouse.x, mouse.y);
match mouse.kind {
MouseKind::Down(MouseButton::Left) => {
let index = index?;
if self.modified_press(cx, flat, index, mouse.mods) {
return Some(true);
}
let key = flat[index].node.key.clone();
if self.double_press(cx, &key) {
self.select_one(cx, flat, index);
self.open_or_activate(cx, index, flat[index].node);
return Some(true);
}
let reduce = self.is_among_many(&key);
if reduce {
self.select(cx, flat, index);
} else {
self.select_one(cx, flat, index);
}
cx.capture_pointer();
*cx.memory::<Spring>() = Spring::default();
let keys = self.carried(&key);
let press = Press { key, keys, start: at, pointer: at, dragging: false, reduce };
*cx.memory::<TreePointer>() = TreePointer { pressed: Some(press), edge: EdgeScroll::default() };
Some(true)
}
MouseKind::Drag(MouseButton::Left) => {
let memory = cx.memory::<TreePointer>();
let press = memory.pressed.as_mut()?;
press.pointer = at;
let starts = !press.dragging && (at.1 - press.start.1).abs() >= 1;
if starts {
press.dragging = true;
}
let dragging = press.dragging;
let keys = press.keys.clone();
let carried_alone = starts && press.reduce && self.dropping.is_none();
if carried_alone {
press.reduce = false;
}
if starts {
self.forget_press(cx);
}
if carried_alone {
self.choose(cx, keys.clone());
}
self.edge_scroll(cx, mouse.y, flat.len(), dragging);
if dragging && self.dropping.is_some() {
let offset = cx.memory::<RowScroll>().offset;
let aim = self.aim(&keys, at, Self::rows_area(cx.area(), flat.len()), offset);
self.wake_for_spring(cx, &aim);
}
Some(true)
}
MouseKind::Up(MouseButton::Left) => {
let memory = cx.memory::<TreePointer>();
memory.edge = EdgeScroll::default();
let press = memory.pressed.take()?;
let area = cx.area();
*cx.memory::<Spring>() = Spring::default();
if press.dragging {
let offset = cx.memory::<RowScroll>().offset;
let aim = self.aim(&press.keys, at, Self::rows_area(area, flat.len()), offset);
self.release(cx, press.keys, aim, mouse.mods.ctrl);
} else if let Some(row) = flat.iter().position(|row| row.node.key == press.key) {
if press.reduce {
self.select_one(cx, flat, row);
}
if self.activate_on == Click::Single {
self.open_or_activate(cx, row, flat[row].node);
}
}
Some(true)
}
_ => None,
}
}
fn wake_for_spring(&self, cx: &mut EventCx<'_, Msg>, aim: &Aim) {
let now = cx.now();
let edge = cx.memory::<TreePointer>().edge.due();
match (self.spring(cx, aim), edge) {
(Some(wait), Some(due)) => cx.repeat_pointer(wait.min(due.saturating_sub(now))),
(Some(wait), None) => cx.repeat_pointer(wait),
(None, Some(_)) => {}
(None, None) => cx.stop_pointer_repeat(),
}
}
pub(super) fn rows_area(area: Rect, total: usize) -> Rect {
let overflows = total > usize::from(area.height);
Rect::new(area.x, area.y, area.width.saturating_sub(u16::from(overflows)), area.height)
}
fn edge_scroll(&self, cx: &mut EventCx<'_, Msg>, y: i32, total: usize, dragging: bool) {
let area = cx.area();
let last = area.bottom() - 1;
let zone = if y <= area.y {
Some(Zone { edge: Edge::Back, beyond: u16::try_from(area.y - y).unwrap_or(u16::MAX) })
} else if y >= last {
Some(Zone { edge: Edge::Forward, beyond: u16::try_from(y - last).unwrap_or(u16::MAX) })
} else {
None
};
let visible = usize::from(area.height);
let mut edge = cx.memory::<TreePointer>().edge;
edge.drive(cx, zone.filter(|_| dragging), |cx, edge| {
let memory = cx.memory::<RowScroll>();
memory.offset = match edge {
Edge::Back => memory.offset.checked_sub(1)?,
Edge::Forward => Some(memory.offset + 1).filter(|next| *next + visible <= total)?,
};
Some(memory.offset)
});
cx.memory::<TreePointer>().edge = edge;
}
fn menu_for(&self, key: &str) -> Option<(ContextMenu<usize>, Vec<Msg>)> {
let items = self.menu.as_ref()?;
let (items, messages) = context_item::keyed(items(key));
Some((ContextMenu::new(items), messages))
}
fn open_menu(&self, cx: &mut EventCx<'_, Msg>, key: &str, anchor: Rect, keyboard: bool) {
let Some((menu, _)) = self.menu_for(key) else {
return;
};
cx.memory::<MenuNode>().0 = Some(key.to_owned());
cx.with_messages(|cx: &mut EventCx<'_, usize>| menu.open(cx, anchor, keyboard));
}
pub(super) fn menu_event(&self, cx: &mut EventCx<'_, Msg>, event: &Event, flat: &[Flat<'_>]) -> bool {
if self.menu.is_none() || flat.is_empty() {
return false;
}
let right_press = match event {
Event::Mouse(mouse) if mouse.kind == MouseKind::Down(MouseButton::Right) => Some((mouse.x, mouse.y)),
_ => None,
};
if context_menu::is_open_in(cx) {
let node = cx.memory::<MenuNode>().0.clone().filter(|key| flat.iter().any(|row| row.node.key == *key));
let elsewhere = right_press.is_some_and(|(x, y)| !context_menu::contains(cx, x, y));
match node.and_then(|key| self.menu_for(&key)).filter(|_| !elsewhere) {
Some((menu, messages)) => {
let (used, chosen) = cx.with_messages(|cx| menu.event(cx, event));
if let Some(message) = chosen.last().and_then(|chosen| messages.into_iter().nth(*chosen)) {
cx.emit(message);
}
return used;
}
None => {
cx.with_messages(|cx: &mut EventCx<'_, usize>| ContextMenu::<usize>::close(cx));
}
}
}
let area = cx.area();
let visible = usize::from(area.height);
match event {
Event::Mouse(_) => {
let Some((x, y)) = right_press else {
return false;
};
let offset = cx.memory::<RowScroll>().offset;
let row = usize::try_from(y - area.y).ok().map(|r| offset + r).filter(|i| *i < flat.len());
let Some(row) = row.filter(|_| x < Self::rows_area(area, flat.len()).right()) else {
return false;
};
if self.is_multi() && !self.is_chosen(&flat[row].node.key) {
self.select_one(cx, flat, row);
}
self.open_menu(cx, &flat[row].node.key, Rect::new(x, y, 1, 1), false);
true
}
Event::Key(key) if context_menu::is_menu_key(key) => {
let Some(row) = self.selected_index(flat) else {
return false;
};
let memory = cx.memory::<RowScroll>();
if row < memory.offset {
memory.offset = row;
} else if visible > 0 && row >= memory.offset + visible {
memory.offset = row + 1 - visible;
}
let y = area.y + i32::try_from(row - memory.offset).unwrap_or(0);
let anchor = Rect::new(area.x, y, Self::rows_area(area, flat.len()).width, 1);
self.open_menu(cx, &flat[row].node.key, anchor, true);
true
}
_ => false,
}
}
pub(super) fn menu_node(&self, cx: &mut PaintCx<'_>) -> Option<String> {
if self.menu.is_none() || !context_menu::is_open(cx) {
return None;
}
cx.memory::<MenuNode>().0.clone()
}
pub(super) fn paint_menu(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
if let Some((menu, _)) = self.menu_node(cx).and_then(|key| self.menu_for(&key)) {
menu.paint_overlay(cx, anchor);
}
}
}