use std::{
cmp::Ordering,
time::{Duration, Instant},
};
use iced::{
advanced::{
layout::{Limits, Node},
overlay, renderer,
renderer::Style,
widget::{tree, tree::Tag, Operation, Tree},
Clipboard, Layout, Renderer as RendererT, Shell, Widget,
},
event::Status,
mouse,
mouse::{Button, Cursor, Interaction},
window,
window::RedrawRequest,
Alignment, BorderRadius, Color, Element, Event, Length, Point, Rectangle, Renderer, Size,
Theme,
};
use keyframe::{functions::EaseOutQuint, keyframes, AnimationSequence};
pub struct ContextMenu<'a, M> {
base: Element<'a, M, Renderer>,
content: Element<'a, M, Renderer>,
on_close: Option<M>,
max_height: f32,
}
impl<'a, M> ContextMenu<'a, M> {
pub fn new(
base: impl Into<Element<'a, M, Renderer>>,
content: impl Into<Element<'a, M, Renderer>>,
) -> Self {
Self {
base: base.into(),
content: content.into(),
on_close: None,
max_height: 400.,
}
}
pub fn on_close(mut self, msg: M) -> Self {
self.on_close = Some(msg);
self
}
}
impl<'a, M: Clone> Widget<M, Renderer> for ContextMenu<'a, M> {
fn width(&self) -> Length {
self.base.as_widget().width()
}
fn height(&self) -> Length {
self.base.as_widget().height()
}
fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node {
self.base.as_widget().layout(renderer, limits)
}
fn draw(
&self,
state: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
self.base.as_widget().draw(
&state.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn tag(&self) -> Tag {
Tag::of::<OverlayState>()
}
fn state(&self) -> tree::State {
tree::State::new(OverlayState {
max_height: self.max_height,
height: 0.,
state: State::Animate(Instant::now(), keyframes![(0.0, 0.0), (400.0, 0.7)]),
})
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.base), Tree::new(&self.content)]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(&[&self.base, &self.content]);
}
fn operate(
&self,
state: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<M>,
) {
self.base
.as_widget()
.operate(state, layout, renderer, operation);
}
fn on_event(
&mut self,
state: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, M>,
viewport: &Rectangle,
) -> Status {
self.base.as_widget_mut().on_event(
&mut state.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
)
}
fn overlay<'b>(
&'b mut self,
state: &'b mut Tree,
layout: Layout<'_>,
_renderer: &Renderer,
) -> Option<overlay::Element<'b, M, Renderer>> {
Some(overlay::Element::new(
layout.position(),
Box::new(Overlay {
content: &mut self.content,
tree: &mut state.children[1],
on_close: self.on_close.clone(),
state: state.state.downcast_mut::<OverlayState>(),
}),
))
}
}
struct Overlay<'a, 'b, M> {
content: &'b mut Element<'a, M, Renderer>,
tree: &'b mut Tree,
on_close: Option<M>,
state: &'b mut OverlayState,
}
impl<'a, 'b, M: Clone> Overlay<'a, 'b, M> {
pub fn handle_mouse_event(
&mut self,
event: &mouse::Event,
layout: &Layout<'_>,
cursor: Cursor,
) -> bool {
if matches!(self.state.state, State::Closed) {
return false;
}
match (&self.state.state, event) {
(State::Dragging(_drag_start, _last_position), mouse::Event::CursorMoved { .. }) => {
true
}
(_, mouse::Event::ButtonPressed(Button::Left)) => {
self.state.state =
State::Dragging(Instant::now(), cursor.position().unwrap_or_default());
true
}
(
State::Dragging(drag_start, _last_position),
mouse::Event::ButtonReleased(Button::Left),
) => {
if drag_start.elapsed() <= Duration::from_millis(100)
&& !cursor.is_over(layout.children().next().unwrap().bounds())
{
self.state.state = State::Animate(
Instant::now(),
keyframes![(self.state.height, 0.0, EaseOutQuint), (0.0, 0.75)],
);
} else if self.state.height <= 0. {
self.state.state = State::Closed;
} else if self.state.height < (self.state.max_height / 1.25) {
self.state.state = State::Animate(
Instant::now(),
keyframes![
(self.state.height, 0.0, EaseOutQuint),
(
0.0,
(0.75 * (self.state.height.abs() / self.state.max_height.abs()))
)
],
);
} else {
self.state.state = State::Animate(
Instant::now(),
keyframes![
(self.state.height, 0.0, EaseOutQuint),
(
self.state.max_height,
(0.75 * (self.state.height.abs() / self.state.max_height.abs()))
)
],
);
}
true
}
_ => false,
}
}
pub fn handle_redraw(&mut self, shell: &mut Shell<M>, cursor: Cursor) {
match &mut self.state.state {
State::Open | State::Closed => {
}
State::Dragging(_, last_position) => {
if let Some(current_position) = cursor.position() {
let dy = current_position.y - last_position.y;
match dy.total_cmp(&0.) {
Ordering::Greater => {
self.state.height -= dy;
self.state.height = self.state.height.max(0.);
}
Ordering::Less if self.state.height > self.state.max_height => {
self.state.height -=
dy / (10. * (self.state.height / self.state.max_height));
}
Ordering::Less => {
self.state.height -= dy;
}
Ordering::Equal => {}
}
*last_position = current_position;
}
}
State::Animate(instant, keyframes) => {
keyframes.advance_by(instant.elapsed().as_secs_f64());
self.state.height = keyframes.now();
*instant = Instant::now();
if keyframes.finished() {
if self.state.height <= 0. {
if let Some(event) = self.on_close.clone() {
shell.publish(event);
}
self.state.state = State::Closed;
} else {
self.state.state = State::Open;
}
} else {
shell.request_redraw(RedrawRequest::NextFrame);
}
}
}
}
}
impl<'a, 'b, M: Clone> overlay::Overlay<M, Renderer> for Overlay<'a, 'b, M> {
fn layout(&self, renderer: &Renderer, bounds: Size, position: Point) -> Node {
let limits = Limits::new(Size::ZERO, bounds)
.width(Length::Fill)
.height(Length::Fill);
let mut child = self.content.as_widget().layout(renderer, &limits);
child.align(Alignment::Start, Alignment::Start, limits.max());
child.move_to(Point {
x: 0.0,
y: bounds.height - self.state.height,
});
let mut node = Node::with_children(bounds, vec![child]);
node.move_to(position);
node
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
style: &Style,
layout: Layout<'_>,
cursor: Cursor,
) {
if matches!(self.state.state, State::Closed) {
return;
}
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
border_radius: BorderRadius::default(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Color {
a: (0.8 / (self.state.max_height / self.state.height)).min(0.9),
..Color::BLACK
},
);
let bounds = Rectangle::new(
Point::new(0.0, layout.bounds().height - self.state.height),
Size {
width: layout.bounds().width,
height: self.state.height,
},
);
renderer.fill_quad(
renderer::Quad {
bounds,
border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::default(),
},
Color::WHITE,
);
self.content.as_widget().draw(
self.tree,
renderer,
theme,
style,
layout.children().next().unwrap(),
cursor,
&bounds,
);
}
fn operate(
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<M>,
) {
self.content.as_widget().operate(
self.tree,
layout.children().next().unwrap(),
renderer,
operation,
);
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, M>,
) -> Status {
let mut status = self.content.as_widget_mut().on_event(
self.tree,
event.clone(),
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
&layout.bounds(),
);
if let (Status::Ignored, Event::Mouse(mouse_event)) = (status, &event) {
let captured = self.handle_mouse_event(mouse_event, &layout, cursor);
if captured {
status = Status::Captured;
if let (Some(msg), State::Closed) = (&self.on_close, &self.state.state) {
shell.publish(msg.clone());
} else {
shell.request_redraw(RedrawRequest::NextFrame);
}
}
} else if let Event::Window(window::Event::RedrawRequested(_)) = &event {
self.handle_redraw(shell, cursor);
}
status
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> Interaction {
self.content.as_widget().mouse_interaction(
self.tree,
layout.children().next().unwrap(),
cursor,
viewport,
renderer,
)
}
fn overlay<'c>(
&'c mut self,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'c, M, Renderer>> {
self.content
.as_widget_mut()
.overlay(self.tree, layout.children().next().unwrap(), renderer)
}
}
impl<'a, M> From<ContextMenu<'a, M>> for Element<'a, M>
where
M: 'a + Clone,
{
fn from(modal: ContextMenu<'a, M>) -> Self {
Element::new(modal)
}
}
#[derive(Default)]
pub struct OverlayState {
max_height: f32,
height: f32,
state: State,
}
#[derive(Default)]
pub enum State {
#[default]
Open,
Animate(Instant, AnimationSequence<f32>),
Dragging(Instant, Point),
Closed,
}