use std::borrow::Cow;
use iced_native::{
alignment,
event::{self, Event},
layout, mouse, overlay,
overlay::menu::{self, Menu},
renderer,
text::{self, Text},
touch,
widget::{
container, scrollable,
tree::{self, Tree},
},
Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Widget,
};
pub use iced_style::pick_list::{Appearance, StyleSheet};
#[allow(missing_debug_implementations)]
pub struct PopupMenu<'a, T, Message, Renderer>
where
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
on_selected: Box<dyn Fn(T) -> Message + 'a>,
options: Cow<'a, [T]>,
width: Length,
padding: Padding,
text_size: Option<f32>,
font: Renderer::Font,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, T: 'a, Message, Renderer> PopupMenu<'a, T, Message, Renderer>
where
T: ToString + Eq,
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
{
pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
pub fn new(
options: impl Into<Cow<'a, [T]>>,
on_selected: impl Fn(T) -> Message + 'a,
) -> Self {
Self {
on_selected: Box::new(on_selected),
options: options.into(),
width: Length::Shrink,
text_size: None,
padding: Self::DEFAULT_PADDING,
font: Default::default(),
style: Default::default(),
}
}
pub fn style(mut self, style: impl Into<<Renderer::Theme as StyleSheet>::Style>) -> Self {
self.style = style.into();
self
}
}
impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> for PopupMenu<'a, T, Message, Renderer>
where
T: Clone + ToString + Eq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer<Font = iced::Font> + 'a,
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<T>>()
}
fn state(&self) -> tree::State {
tree::State::new(State::<T>::new())
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
layout(
renderer,
limits,
self.width,
self.padding,
self.text_size,
&self.font,
)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
update(
event,
layout,
cursor_position,
shell,
self.on_selected.as_ref(),
None,
&self.options,
|| tree.state.downcast_mut::<State<T>>(),
)
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(layout, cursor_position, !self.options.is_empty())
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
draw(
renderer,
theme,
layout,
cursor_position,
&self.style,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
_renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
let state = tree.state.downcast_mut::<State<T>>();
overlay(
layout,
state,
self.padding,
self.text_size,
self.font,
&self.options,
self.style.clone(),
)
}
}
impl<'a, T: 'a, Message, Renderer> From<PopupMenu<'a, T, Message, Renderer>> for Element<'a, Message, Renderer>
where
T: Clone + ToString + Eq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer<Font = iced::Font> + 'a,
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
{
fn from(pick_list: PopupMenu<'a, T, Message, Renderer>) -> Self {
Self::new(pick_list)
}
}
#[derive(Debug)]
pub struct State<T> {
menu: menu::State,
is_open: bool,
hovered_option: Option<usize>,
last_selection: Option<T>,
}
impl<T> State<T> {
pub fn new() -> Self {
Self {
menu: menu::State::default(),
is_open: bool::default(),
hovered_option: Option::default(),
last_selection: Option::default(),
}
}
}
impl<T> Default for State<T> {
fn default() -> Self {
Self::new()
}
}
pub fn layout<Renderer>(
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
padding: Padding,
text_size: Option<f32>,
font: &Renderer::Font,
) -> layout::Node
where
Renderer: text::Renderer,
{
use std::f32;
let limits = limits.width(width).height(Length::Shrink).pad(padding);
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
let max_width = match width {
Length::Shrink => {
let (width, _) = renderer.measure(
&crate::gui::icon::Icon::MoreVert.as_char().to_string(),
text_size,
font.clone(),
Size::new(f32::INFINITY, f32::INFINITY),
);
width.round() as u32
}
_ => 0,
};
let size = {
let intrinsic = Size::new(max_width as f32 + text_size + padding.left, text_size);
limits.resolve(intrinsic).pad(padding)
};
layout::Node::new(Size::new(size.width, 24.0))
}
pub fn update<'a, T, Message>(
event: Event,
layout: Layout<'_>,
cursor_position: Point,
shell: &mut Shell<'_, Message>,
on_selected: &dyn Fn(T) -> Message,
selected: Option<&T>,
options: &[T],
state: impl FnOnce() -> &'a mut State<T>,
) -> event::Status
where
T: PartialEq + Clone + 'a,
{
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state();
let event_status = if state.is_open {
state.is_open = false;
event::Status::Captured
} else if layout.bounds().contains(cursor_position) {
state.is_open = !options.is_empty();
state.hovered_option = options.iter().position(|option| Some(option) == selected);
event::Status::Captured
} else {
event::Status::Ignored
};
if let Some(last_selection) = state.last_selection.take() {
shell.publish((on_selected)(last_selection));
state.is_open = false;
event::Status::Captured
} else {
event_status
}
}
Event::Mouse(mouse::Event::WheelScrolled { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => {
let state = state();
state.is_open = false;
event::Status::Ignored
}
_ => event::Status::Ignored,
}
}
pub fn mouse_interaction(layout: Layout<'_>, cursor_position: Point, usable: bool) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
if is_mouse_over && usable {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
pub fn overlay<'a, T, Message, Renderer>(
layout: Layout<'_>,
state: &'a mut State<T>,
padding: Padding,
text_size: Option<f32>,
font: Renderer::Font,
options: &'a [T],
style: <Renderer::Theme as StyleSheet>::Style,
) -> Option<overlay::Element<'a, Message, Renderer>>
where
T: Clone + ToString,
Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet + scrollable::StyleSheet + menu::StyleSheet + container::StyleSheet,
<Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
{
if state.is_open {
let bounds = layout.bounds();
let mut menu = Menu::new(
&mut state.menu,
options,
&mut state.hovered_option,
&mut state.last_selection,
)
.width(150.0)
.padding(padding)
.font(font)
.style(style);
if let Some(text_size) = text_size {
menu = menu.text_size(text_size);
}
Some(menu.overlay(layout.position(), bounds.height))
} else {
None
}
}
pub fn draw<Renderer>(
renderer: &mut Renderer,
theme: &Renderer::Theme,
layout: Layout<'_>,
cursor_position: Point,
style: &<Renderer::Theme as StyleSheet>::Style,
) where
Renderer: text::Renderer<Font = iced::Font>,
Renderer::Theme: StyleSheet,
{
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let style = if is_mouse_over {
theme.hovered(style)
} else {
theme.active(style)
};
if is_mouse_over {
renderer.fill_quad(
renderer::Quad {
bounds,
border_color: style.border_color,
border_width: style.border_width,
border_radius: renderer::BorderRadius::from(style.border_radius),
},
style.background,
);
}
let icon_size = 0.5;
renderer.fill_text(Text {
content: &crate::gui::icon::Icon::MoreVert.as_char().to_string(),
font: crate::gui::icon::ICONS,
size: bounds.height * icon_size * 1.5,
bounds: Rectangle {
x: bounds.center_x(),
y: bounds.center_y(),
..bounds
},
color: style.text_color,
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
});
}