#![deny(missing_docs)]
use std::{any::Any, rc::Rc};
use floem_reactive::{
as_child_of_current_scope, create_effect, create_updater, Scope, SignalGet, SignalUpdate,
};
use floem_winit::keyboard::{Key, NamedKey};
use peniko::{
kurbo::{Point, Rect},
Color,
};
use crate::{
action::{add_overlay, remove_overlay},
event::{Event, EventListener, EventPropagation},
id::ViewId,
prop, prop_extractor,
style::{CustomStylable, Style, StyleClass, Width},
style_class,
unit::PxPctAuto,
view::{default_compute_layout, IntoView, View},
views::{container, scroll, stack, svg, text, Decorators},
AnyView,
};
use super::list;
type ChildFn<T> = dyn Fn(T) -> (AnyView, Scope);
type ListViewFn<T> = Rc<dyn Fn(&dyn Fn(T) -> AnyView) -> AnyView>;
style_class!(
pub DropdownClass
);
prop!(
pub CloseOnAccept: bool {} = true
);
prop_extractor!(DropdownStyle {
close_on_accept: CloseOnAccept,
});
pub struct Dropdown<T: 'static> {
id: ViewId,
current_value: T,
main_view: ViewId,
main_view_scope: Scope,
main_fn: Box<ChildFn<T>>,
list_view: ListViewFn<T>,
list_item_fn: Rc<dyn Fn(T) -> AnyView>,
list_style: Style,
overlay_id: Option<ViewId>,
window_origin: Option<Point>,
on_accept: Option<Box<dyn Fn(T)>>,
on_open: Option<Box<dyn Fn(bool)>>,
style: DropdownStyle,
}
enum Message {
OpenState(bool),
ActiveElement(Box<dyn Any>),
ListFocusLost,
ListSelect(Box<dyn Any>),
}
impl<T: 'static + Clone> View for Dropdown<T> {
fn id(&self) -> ViewId {
self.id
}
fn debug_name(&self) -> std::borrow::Cow<'static, str> {
"DropDown".into()
}
fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
if self.style.read(cx) {
cx.app_state_mut().request_paint(self.id);
}
self.list_style = cx
.indirect_style()
.clone()
.apply_classes_from_context(&[scroll::ScrollClass::class_ref()], cx.indirect_style());
for child in self.id.children() {
cx.style_view(child);
}
}
fn compute_layout(&mut self, cx: &mut crate::context::ComputeLayoutCx) -> Option<Rect> {
self.window_origin = Some(cx.window_origin);
default_compute_layout(self.id, cx)
}
fn update(&mut self, cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
if let Ok(state) = state.downcast::<Message>() {
match *state {
Message::OpenState(true) => self.open_dropdown(cx),
Message::OpenState(false) => self.close_dropdown(),
Message::ListFocusLost => self.close_dropdown(),
Message::ListSelect(val) => {
if let Ok(val) = val.downcast::<T>() {
if self.style.close_on_accept() {
self.close_dropdown();
}
if let Some(on_select) = &self.on_accept {
on_select(*val);
}
}
}
Message::ActiveElement(val) => {
if let Ok(val) = val.downcast::<T>() {
let old_child_scope = self.main_view_scope;
let old_main_view = self.main_view;
let (main_view, main_view_scope) = (self.main_fn)(*val);
let main_view_id = main_view.id();
self.id.set_children(vec![main_view]);
self.main_view = main_view_id;
self.main_view_scope = main_view_scope;
cx.app_state_mut().remove_view(old_main_view);
old_child_scope.dispose();
self.id.request_all();
}
}
}
}
}
fn event_before_children(
&mut self,
_cx: &mut crate::context::EventCx,
event: &Event,
) -> EventPropagation {
match event {
Event::PointerDown(_) => {
self.swap_state();
return EventPropagation::Stop;
}
Event::KeyUp(ref key_event)
if matches!(
key_event.key.logical_key,
Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space)
) =>
{
self.swap_state()
}
_ => {}
}
EventPropagation::Continue
}
}
impl<T: Clone> Dropdown<T> {
pub fn default_main_view(item: T) -> AnyView
where
T: std::fmt::Display,
{
const CHEVRON_DOWN: &str = r##"
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 185.344 185.344">
<path fill="#010002" d="M92.672 144.373a10.707 10.707 0 0 1-7.593-3.138L3.145 59.301c-4.194-4.199
-4.194-10.992 0-15.18a10.72 10.72 0 0 1 15.18 0l74.347 74.341 74.347-74.341a10.72 10.72 0 0 1
15.18 0c4.194 4.194 4.194 10.981 0 15.18l-81.939 81.934a10.694 10.694 0 0 1-7.588 3.138z"/>
</svg>
"##;
stack((
text(item),
container(svg(CHEVRON_DOWN).style(|s| s.size(12, 12).color(Color::BLACK))).style(|s| {
s.items_center()
.padding(3.)
.border_radius(5)
.hover(move |s| s.background(Color::LIGHT_GRAY))
}),
))
.style(|s| s.items_center().justify_between().size_full())
.into_any()
}
pub fn custom<MF, I, LF, AIF>(
active_item: AIF,
main_view: MF,
iterator: I,
list_item_fn: LF,
) -> Dropdown<T>
where
MF: Fn(T) -> AnyView + 'static,
I: IntoIterator<Item = T> + Clone + 'static,
LF: Fn(T) -> AnyView + Clone + 'static,
T: Clone + 'static,
AIF: Fn() -> T + 'static,
{
let dropdown_id = ViewId::new();
let list_item_fn = Rc::new(list_item_fn);
let list_view = Rc::new(move |list_item_fn: &dyn Fn(T) -> AnyView| {
let iterator = iterator.clone();
let iter_clone = iterator.clone();
let inner_list = list(iterator.into_iter().map(list_item_fn))
.on_accept(move |opt_idx| {
if let Some(idx) = opt_idx {
let val = iter_clone.clone().into_iter().nth(idx).unwrap();
dropdown_id.update_state(Message::ActiveElement(Box::new(val.clone())));
dropdown_id.update_state(Message::ListSelect(Box::new(val)));
}
})
.style(|s| s.size_full())
.keyboard_navigable()
.on_event_stop(EventListener::PointerDown, move |_| {})
.on_event_stop(EventListener::FocusLost, move |_| {
dropdown_id.update_state(Message::ListFocusLost);
});
let inner_list_id = inner_list.id();
scroll(inner_list)
.on_event_stop(EventListener::FocusGained, move |_| {
inner_list_id.request_focus();
})
.into_any()
});
let initial = create_updater(active_item, move |new_state| {
dropdown_id.update_state(Message::ActiveElement(Box::new(new_state)));
});
let main_fn = Box::new(as_child_of_current_scope(main_view));
let (child, main_view_scope) = main_fn(initial.clone());
let main_view = child.id();
dropdown_id.set_children(vec![child]);
Self {
id: dropdown_id,
current_value: initial,
main_view,
main_view_scope,
main_fn,
list_view,
list_item_fn,
list_style: Style::new(),
overlay_id: None,
window_origin: None,
on_accept: None,
on_open: None,
style: Default::default(),
}
.class(DropdownClass)
}
pub fn new<AIF, I>(active_item: AIF, iterator: I) -> Dropdown<T>
where
AIF: Fn() -> T + 'static,
I: IntoIterator<Item = T> + Clone + 'static,
T: Clone + std::fmt::Display + 'static,
{
Self::custom(active_item, Self::default_main_view, iterator, |v| {
crate::views::text(v).into_any()
})
}
pub fn new_rw<AI, I>(active_item: AI, iterator: I) -> Dropdown<T>
where
AI: SignalGet<T> + SignalUpdate<T> + Copy + 'static,
I: IntoIterator<Item = T> + Clone + 'static,
T: Clone + std::fmt::Display + 'static,
{
Self::custom(
move || active_item.get(),
Self::default_main_view,
iterator,
|t| text(t).into_any(),
)
.on_accept(move |nv| active_item.set(nv))
}
pub fn main_view(mut self, main_view: impl Fn(T) -> Box<dyn View> + 'static) -> Self {
self.main_fn = Box::new(as_child_of_current_scope(main_view));
let (child, main_view_scope) = (self.main_fn)(self.current_value.clone());
let main_view = child.id();
self.main_view_scope = main_view_scope;
self.main_view = main_view;
self.id.set_children(vec![child]);
self
}
pub fn list_item_view(mut self, list_item_fn: impl Fn(T) -> Box<dyn View> + 'static) -> Self {
self.list_item_fn = Rc::new(list_item_fn);
self
}
pub fn show_list(self, show: impl Fn() -> bool + 'static) -> Self {
let id = self.id();
create_effect(move |_| {
let state = show();
id.update_state(Message::OpenState(state));
});
self
}
pub fn on_accept(mut self, on_accept: impl Fn(T) + 'static) -> Self {
self.on_accept = Some(Box::new(on_accept));
self
}
pub fn on_open(mut self, on_open: impl Fn(bool) + 'static) -> Self {
self.on_open = Some(Box::new(on_open));
self
}
fn swap_state(&self) {
if self.overlay_id.is_some() {
self.id.update_state(Message::OpenState(false));
} else {
self.id.request_layout();
self.id.update_state(Message::OpenState(true));
}
}
fn open_dropdown(&mut self, cx: &mut crate::context::UpdateCx) {
if self.overlay_id.is_none() {
self.id.request_layout();
cx.app_state.compute_layout();
if let Some(layout) = self.id.get_layout() {
self.update_list_style(layout.size.width as f64);
let point =
self.window_origin.unwrap_or_default() + (0., layout.size.height as f64);
self.create_overlay(point);
if let Some(on_open) = &self.on_open {
on_open(true);
}
}
}
}
fn close_dropdown(&mut self) {
if let Some(id) = self.overlay_id.take() {
remove_overlay(id);
if let Some(on_open) = &self.on_open {
on_open(false);
}
}
}
fn update_list_style(&mut self, width: f64) {
if let PxPctAuto::Pct(pct) = self.list_style.get(Width) {
let new_width = width * pct / 100.0;
self.list_style = self.list_style.clone().width(new_width);
}
}
fn create_overlay(&mut self, point: Point) {
let list = self.list_view.clone();
let list_style = self.list_style.clone();
let list_item_fn = self.list_item_fn.clone();
self.overlay_id = Some(add_overlay(point, move |_| {
let list = list(&*list_item_fn.clone())
.style(move |s| s.apply(list_style.clone()))
.into_view();
let list_id = list.id();
list_id.request_focus();
list
}));
}
pub fn dropdown_style(
self,
style: impl Fn(DropdownCustomStyle) -> DropdownCustomStyle + 'static,
) -> Self {
self.custom_style(style)
}
}
#[derive(Debug, Clone, Default)]
pub struct DropdownCustomStyle(Style);
impl From<DropdownCustomStyle> for Style {
fn from(val: DropdownCustomStyle) -> Self {
val.0
}
}
impl<T: Clone> CustomStylable<DropdownCustomStyle> for Dropdown<T> {
type DV = Self;
}
impl DropdownCustomStyle {
pub fn new() -> Self {
Self::default()
}
pub fn close_on_accept(mut self, close: bool) -> Self {
self = Self(self.0.set(CloseOnAccept, close));
self
}
}
impl<T> Drop for Dropdown<T> {
fn drop(&mut self) {
if let Some(id) = self.overlay_id {
remove_overlay(id)
}
}
}