use freya_animation::prelude::*;
use freya_core::prelude::*;
use torin::prelude::*;
use crate::{
get_theme,
icons::arrow::ArrowIcon,
theming::component_themes::{
DropdownItemThemePartial,
DropdownThemePartial,
},
};
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum DropdownItemStatus {
#[default]
Idle,
Hovering,
}
#[derive(Clone, PartialEq)]
pub struct DropdownItem {
pub(crate) theme: Option<DropdownItemThemePartial>,
pub selected: bool,
pub on_press: Option<EventHandler<Event<PressEventData>>>,
pub children: Vec<Element>,
pub key: DiffKey,
}
impl ChildrenExt for DropdownItem {
fn get_children(&mut self) -> &mut Vec<Element> {
&mut self.children
}
}
impl KeyExt for DropdownItem {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl Default for DropdownItem {
fn default() -> Self {
Self::new()
}
}
impl DropdownItem {
pub fn new() -> Self {
Self {
theme: None,
selected: false,
on_press: None,
children: Vec::new(),
key: DiffKey::None,
}
}
pub fn theme(mut self, theme: DropdownItemThemePartial) -> Self {
self.theme = Some(theme);
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn on_press(mut self, handler: impl FnMut(Event<PressEventData>) + 'static) -> Self {
self.on_press = Some(EventHandler::new(handler));
self
}
}
impl Render for DropdownItem {
fn render(&self) -> impl IntoElement {
let theme = get_theme!(&self.theme, dropdown_item);
let focus = use_focus();
let focus_status = use_focus_status(focus);
let mut status = use_state(DropdownItemStatus::default);
let dropdown_group = use_consume::<DropdownGroup>();
use_drop(move || {
if status() == DropdownItemStatus::Hovering {
Cursor::set(CursorIcon::default());
}
});
let background = if self.selected {
theme.select_background
} else if *status.read() == DropdownItemStatus::Hovering {
theme.hover_background
} else {
theme.background
};
let border = if focus_status() == FocusStatus::Keyboard {
Border::new()
.fill(theme.select_border_fill)
.width(2.)
.alignment(BorderAlignment::Inner)
} else {
Border::new()
.fill(theme.border_fill)
.width(1.)
.alignment(BorderAlignment::Inner)
};
rect()
.width(Size::fill_minimum())
.color(theme.color)
.a11y_id(focus.a11y_id())
.a11y_focusable(Focusable::Enabled)
.a11y_member_of(dropdown_group.group_id)
.a11y_role(AccessibilityRole::ListBoxOption)
.background(background)
.border(border)
.corner_radius(6.)
.padding((6., 10., 6., 10.))
.main_align(Alignment::center())
.on_pointer_enter(move |_| {
*status.write() = DropdownItemStatus::Hovering;
Cursor::set(CursorIcon::Pointer);
})
.on_pointer_leave(move |_| {
*status.write() = DropdownItemStatus::Idle;
Cursor::set(CursorIcon::default());
})
.map(self.on_press.clone(), |rect, on_press| {
rect.on_press(on_press)
})
.children(self.children.clone())
}
fn render_key(&self) -> DiffKey {
self.key.clone().or(self.default_key())
}
}
#[derive(Clone)]
struct DropdownGroup {
group_id: AccessibilityId,
}
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum DropdownStatus {
#[default]
Idle,
Hovering,
}
#[cfg_attr(feature = "docs",
doc = embed_doc_image::embed_image!("dropdown", "images/gallery_dropdown.png")
)]
#[derive(Clone, PartialEq)]
pub struct Dropdown {
pub(crate) theme: Option<DropdownThemePartial>,
pub selected_item: Option<Element>,
pub children: Vec<Element>,
pub key: DiffKey,
}
impl ChildrenExt for Dropdown {
fn get_children(&mut self) -> &mut Vec<Element> {
&mut self.children
}
}
impl Default for Dropdown {
fn default() -> Self {
Self::new()
}
}
impl Dropdown {
pub fn new() -> Self {
Self {
theme: None,
selected_item: None,
children: Vec::new(),
key: DiffKey::None,
}
}
pub fn theme(mut self, theme: DropdownThemePartial) -> Self {
self.theme = Some(theme);
self
}
pub fn selected_item(mut self, item: impl Into<Element>) -> Self {
self.selected_item = Some(item.into());
self
}
pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
self.key = key.into();
self
}
}
impl Render for Dropdown {
fn render(&self) -> impl IntoElement {
let theme = get_theme!(&self.theme, dropdown);
let focus = use_focus();
let focus_status = use_focus_status(focus);
let mut status = use_state(DropdownStatus::default);
let mut open = use_state(|| false);
use_provide_context(|| DropdownGroup {
group_id: focus.a11y_id(),
});
let animation = use_animation(move |conf| {
conf.on_change(OnChange::Rerun);
conf.on_creation(OnCreation::Finish);
let scale = AnimNum::new(0.8, 1.)
.time(350)
.ease(Ease::Out)
.function(Function::Expo);
let opacity = AnimNum::new(0., 1.)
.time(350)
.ease(Ease::Out)
.function(Function::Expo);
if open() {
(scale, opacity)
} else {
(scale.into_reversed(), opacity.into_reversed())
}
});
use_drop(move || {
if status() == DropdownStatus::Hovering {
Cursor::set(CursorIcon::default());
}
});
use_side_effect(move || {
if let Some(member_of) = Platform::get()
.focused_accessibility_node
.read()
.member_of()
{
if member_of != focus.a11y_id() {
open.set_if_modified(false);
}
} else {
open.set_if_modified(false);
}
});
let on_press = move |e: Event<PressEventData>| {
focus.request_focus();
open.toggle();
e.prevent_default();
e.stop_propagation();
};
let on_pointer_enter = move |_| {
*status.write() = DropdownStatus::Hovering;
Cursor::set(CursorIcon::Pointer);
};
let on_pointer_leave = move |_| {
*status.write() = DropdownStatus::Idle;
Cursor::set(CursorIcon::default());
};
let on_global_mouse_up = move |_| {
open.set_if_modified(false);
};
let on_global_key_down = move |e: Event<KeyboardEventData>| match e.key {
Key::Escape => {
open.set_if_modified(false);
}
Key::Enter if focus.is_focused() => {
open.toggle();
}
_ => {}
};
let (scale, opacity) = animation.read().value();
let background = match *status.read() {
DropdownStatus::Hovering => theme.hover_background,
DropdownStatus::Idle => theme.background_button,
};
let border = if focus_status() == FocusStatus::Keyboard {
Border::new()
.fill(theme.focus_border_fill)
.width(2.)
.alignment(BorderAlignment::Inner)
} else {
Border::new()
.fill(theme.border_fill)
.width(1.)
.alignment(BorderAlignment::Inner)
};
rect()
.child(
rect()
.a11y_id(focus.a11y_id())
.a11y_member_of(focus.a11y_id())
.a11y_role(AccessibilityRole::ListBox)
.a11y_focusable(Focusable::Enabled)
.on_pointer_enter(on_pointer_enter)
.on_pointer_leave(on_pointer_leave)
.on_press(on_press)
.on_global_key_down(on_global_key_down)
.on_global_mouse_up(on_global_mouse_up)
.width(theme.width)
.margin(theme.margin)
.background(background)
.padding((6., 16., 6., 16.))
.border(border)
.horizontal()
.center()
.color(theme.color)
.corner_radius(8.)
.maybe_child(self.selected_item.clone())
.child(
ArrowIcon::new()
.margin((0., 0., 0., 8.))
.rotate(0.)
.fill(theme.arrow_fill),
),
)
.maybe_child((open() || opacity > 0.).then(|| {
rect().height(Size::px(0.)).width(Size::px(0.)).child(
rect()
.width(Size::window_percent(100.))
.margin(Gaps::new(4., 0., 0., 0.))
.child(
rect()
.layer(Layer::Overlay)
.border(
Border::new()
.fill(theme.border_fill)
.width(1.)
.alignment(BorderAlignment::Inner),
)
.overflow(Overflow::Clip)
.corner_radius(8.)
.background(theme.dropdown_background)
.padding(6.)
.content(Content::Fit)
.opacity(opacity)
.scale(scale)
.children(self.children.clone()),
),
)
}))
}
fn render_key(&self) -> DiffKey {
self.key.clone().or(self.default_key())
}
}