use super::{defaults::*, globals::*, style::*, utils::*};
use crate::impl_syncers;
use bevy_asset::prelude::*;
use bevy_color::prelude::*;
use bevy_ecs::{prelude::*, system::SystemId};
use bevy_text::prelude::*;
use bevy_ui::prelude::*;
use haalka::futures_signals::{prelude::*, raw::utils::remove_system_holder_on_remove};
use std::{
fmt::Display,
sync::{Arc, OnceLock},
};
#[derive(Default)]
pub struct DynamicText {
el: El<Text>,
text: Mutable<String>,
font: Mutable<Handle<Font>>,
font_size: Mutable<f32>,
color: Mutable<Color>,
}
impl ElementWrapper for DynamicText {
type EL = El<Text>;
fn element_mut(&mut self) -> &mut Self::EL {
&mut self.el
}
}
impl CursorOnHoverable for DynamicText {}
impl GlobalEventAware for DynamicText {}
impl PointerEventAware for DynamicText {}
impl DynamicText {
pub fn new() -> Self {
let text = Mutable::new(String::new());
let font = Mutable::new(Default::default());
let font_size = Mutable::new(DEFAULT_FONT_SIZE);
let color = Mutable::new(DEFAULT_UNHIGHLIGHTED_COLOR);
let el: El<Text> = El::<Text>::new()
.text_font(
TextFont::default()
.with_font(font.get_cloned())
.with_font_size(font_size.get()),
)
.text_color(TextColor(color.get()))
.text(Text(text.get_cloned()))
.text_signal(text.signal_cloned().dedupe_cloned().map(Text))
.on_signal_with_text_font(font.signal_cloned(), |mut text_font, font| {
text_font.font = font;
})
.apply(text_style(font_size.signal(), color.signal()));
Self {
el,
text,
font,
font_size,
color,
}
}
impl_syncers! { text: String, font: Handle<Font>, font_size: f32, color: Color }
}
pub struct HighlightableText {
pub text: DynamicText,
highlighted_color: Mutable<Color>,
unhighlighted_color: Mutable<Color>,
highlighted: Mutable<bool>,
}
impl ElementWrapper for HighlightableText {
type EL = DynamicText;
fn element_mut(&mut self) -> &mut Self::EL {
&mut self.text
}
}
impl GlobalEventAware for HighlightableText {}
impl PointerEventAware for HighlightableText {}
impl HighlightableText {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let unhighlighted_color = Mutable::new(DEFAULT_UNHIGHLIGHTED_COLOR);
let highlighted_color = Mutable::new(DEFAULT_HIGHLIGHTED_COLOR);
let hovered = Mutable::new(false);
let highlighted = Mutable::new(false);
let dynamic_text = DynamicText::new()
.color_signal(map_bool_signal(
signal::or(hovered.signal(), highlighted.signal()),
highlighted_color.clone(),
unhighlighted_color.clone(),
))
.hovered_sync(hovered);
Self {
text: dynamic_text,
highlighted_color,
unhighlighted_color,
highlighted,
}
}
pub fn with_text(mut self, f: impl FnOnce(DynamicText) -> DynamicText) -> Self {
self.text = f(self.text);
self
}
impl_syncers! {
highlighted_color: Color,
unhighlighted_color: Color,
highlighted: bool
}
}
pub struct Checkbox {
el: El<Node>,
size: Mutable<f32>,
background_color: Mutable<Color>,
highlighted_color: Mutable<Color>,
unhighlighted_color: Mutable<Color>,
border_radius: Mutable<f32>,
hovered: Mutable<bool>,
checked: Mutable<bool>,
}
impl ElementWrapper for Checkbox {
type EL = El<Node>;
fn element_mut(&mut self) -> &mut Self::EL {
&mut self.el
}
}
impl GlobalEventAware for Checkbox {}
impl PointerEventAware for Checkbox {}
impl Nameable for Checkbox {}
const CHECKBOX_BORDER_RADIUS_MODIFIER: f32 = 0.333;
impl Checkbox {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let size = GLOBAL_FONT_SIZE.clone();
let background_color = GLOBAL_SECONDARY_BACKGROUND_COLOR.clone();
let highlighted_color = GLOBAL_HIGHLIGHTED_COLOR.clone();
let unhighlighted_color = GLOBAL_UNHIGHLIGHTED_COLOR.clone();
let border_radius_base = GLOBAL_BORDER_RADIUS.get() * CHECKBOX_BORDER_RADIUS_MODIFIER;
let border_width = GLOBAL_BORDER_WIDTH.clone();
let border_radius = Mutable::new(border_radius_base);
let hovered = Mutable::new(false);
let checked = Mutable::new(false);
let el = El::<Node>::new()
.align_content(Align::center())
.apply(square_style(size.signal()))
.apply(border_radius_style(BoxCorner::ALL, border_radius.signal()))
.apply(border_width_style(BoxEdge::ALL, border_width.signal()))
.apply(border_color_style(hovered.signal().map_bool_signal(clone!((highlighted_color) move || highlighted_color.signal()), || GLOBAL_BORDER_COLOR.signal())))
.hovered_sync(hovered.clone())
.cursor(CursorIcon::System(SystemCursorIcon::Pointer))
.apply(padding_style(BoxEdge::ALL, always(1.)))
.child_signal(
checked.signal()
.map_true(clone!((hovered, highlighted_color, unhighlighted_color, border_radius) move ||
El::<Node>::new()
.apply(border_radius_style(BoxCorner::ALL, border_radius.signal().map(mul(0.5))))
.with_node(|mut node| {
node.width = Val::Percent(100.);
node.height = Val::Percent(100.);
})
.apply(background_style(hovered.signal().map_bool_signal(clone!((highlighted_color) move || highlighted_color.signal()), clone!((unhighlighted_color) move || unhighlighted_color.signal()))))
))
);
Self {
el,
size,
background_color,
highlighted_color,
unhighlighted_color,
border_radius,
hovered,
checked,
}
}
impl_syncers! {
size: f32,
background_color: Color,
highlighted_color: Color,
unhighlighted_color: Color,
border_radius: f32,
hovered: bool,
checked: bool,
}
}
#[derive(Clone)]
pub struct OptionData<T> {
pub option: T,
pub blocked: bool,
filtered: Mutable<bool>,
}
impl<T> OptionData<T> {
pub fn new(option: T, blocked: bool) -> Self {
Self {
option,
blocked,
filtered: Mutable::new(false),
}
}
}
pub struct Dropdown<T> {
el: El<Node>,
options: MutableVec<OptionData<T>>,
option_handler_system: Arc<OnceLock<SystemId<In<usize>>>>,
blocked_tooltip: Option<String>,
selected: Mutable<Option<usize>>,
show_dropdown: Mutable<bool>,
font_size: Mutable<f32>,
padding: Mutable<f32>,
border_radius: Mutable<f32>,
border_width: Mutable<f32>,
background_color: Mutable<Color>,
highlighted_color: Mutable<Color>,
unhighlighted_color: Mutable<Color>,
border_color: Mutable<Color>,
error_color: Mutable<Color>,
}
impl<T: Clone + PartialEq + Display + Send + Sync + 'static> ElementWrapper for Dropdown<T> {
type EL = El<Node>;
fn element_mut(&mut self) -> &mut Self::EL {
&mut self.el
}
fn into_el(self) -> Self::EL {
let Self {
el,
options,
option_handler_system,
blocked_tooltip,
selected,
show_dropdown,
font_size,
padding,
border_radius,
border_width,
background_color,
highlighted_color,
unhighlighted_color,
border_color,
error_color,
} = self;
let hovered = Mutable::new(false);
let expected_tooltip_height = font_size.get() + padding.get() + border_width.get() * 2. + 3.;
let blocked_tooltip = Arc::new(blocked_tooltip);
el
.child(
El::<Node>::new()
.apply(border_color_style(signal::and(hovered.signal(), signal::not(show_dropdown.signal())).map_bool_signal(clone!((highlighted_color) move || highlighted_color.signal()), clone!((border_color) move || border_color.signal()))))
.apply(border_width_style(BoxEdge::ALL, border_width.signal()))
.apply(border_radius_style(BoxCorner::TOP, border_radius.signal()))
.apply(border_radius_style(BoxCorner::BOTTOM, show_dropdown.signal().map_false_signal(clone!((border_radius) move || border_radius.signal())).map(Option::unwrap_or_default)))
.with_node(|mut node| node.width = Val::Percent(100.))
.apply(background_style(background_color.signal()))
.cursor(CursorIcon::System(SystemCursorIcon::Pointer))
.on_click(clone!((show_dropdown) move || flip(&show_dropdown)))
.hovered_sync(hovered.clone())
.child(
El::<Node>::new()
.with_node(|mut node| node.width = Val::Percent(100.))
.apply(border_radius_style(BoxCorner::ALL, border_radius.signal()))
.apply(border_style(border_width.signal(), signal::and(show_dropdown.signal(), hovered.signal()).map_bool_signal(clone!((highlighted_color) move || highlighted_color.signal()), clone!((background_color) move || background_color.signal()))))
.apply(padding_style(BoxEdge::ALL, padding.signal()))
.child(
El::<Text>::new()
.text_font_signal(font_size.signal().map(TextFont::from_font_size))
.text_color_signal(unhighlighted_color.signal().map(TextColor))
.text_signal(
selected.signal()
.map_some(clone!((options) move |i|
options.signal_vec_cloned().to_signal_map(move |options| options.get(i).cloned())
))
.map(signal::option).flatten().map(Option::flatten)
.map(|option_option| option_option.map(|option| option.option.to_string()).unwrap_or_default())
.map(Text)
)
.apply(text_style(font_size.signal(), hovered.signal().map_bool_signal(clone!((highlighted_color) move || highlighted_color.signal()), clone!((unhighlighted_color) move || unhighlighted_color.signal()))))
)
)
)
.child_signal(show_dropdown.signal().map_true(
clone!((options, selected, blocked_tooltip) move || {
let option_handler_system = option_handler_system.get().copied();
Column::<Node>::new()
.global_z_index(GlobalZIndex(z_order("dropdown")))
.apply(border_color_style(border_color.signal()))
.with_node(|mut node| {
node.width = Val::Percent(100.);
node.position_type = PositionType::Absolute;
})
.update_raw_el(|raw_el| raw_el.on_spawn_with_system(|In(entity), child_ofs: Query<&ChildOf>, childrens: Query<&Children>, computed_nodes: Query<&ComputedNode>, mut nodes: Query<&mut Node>| {
if let Ok(child_of) = child_ofs.get(entity)
&& let Ok(siblings) = childrens.get(child_of.parent())
&& let Some(&sibling) = siblings.first()
&& let Ok(sibling_node) = computed_nodes.get(sibling)
&& let Ok(mut node) = nodes.get_mut(entity) {
node.top = Val::Px(sibling_node.size().y + 1.); }
}))
.apply(border_width_style([BoxEdge::Left, BoxEdge::Right, BoxEdge::Bottom], border_width.signal()))
.apply(border_radius_style([BoxCorner::BottomLeft, BoxCorner::BottomRight], border_radius.signal()))
.apply(background_style(background_color.signal()))
.align_content(Align::center())
.items_signal_vec(
options.signal_vec_cloned()
.enumerate()
.sort_by_cloned(|(_, left), (_, right)| left.blocked.cmp(&right.blocked))
.filter_signal_cloned(clone!((selected) move |(i, OptionData { filtered, .. })| signal::and(signal::not(signal_eq(i.signal(), selected.signal())), signal::not(filtered.signal()))))
.map(clone!((
font_size,
unhighlighted_color,
background_color,
padding,
border_width,
border_radius,
highlighted_color,
error_color,
blocked_tooltip
) move |(i, OptionData { option, blocked, .. })| {
let hovered = Mutable::new(false);
let mut el = El::<Node>::new()
.with_node(|mut node| node.width = Val::Percent(100.))
.apply(border_radius_style(BoxCorner::ALL, border_radius.signal()))
.apply(background_style(background_color.signal()))
.apply(padding_style(BoxEdge::ALL, padding.signal()))
.apply(border_width_style(BoxEdge::ALL, border_width.signal()))
.apply(
border_style(
border_width.signal(),
hovered.signal().map_bool_signal(
clone!((error_color, highlighted_color) move || if blocked { error_color.signal() } else { highlighted_color.signal() }),
clone!((background_color) move || background_color.signal())
)
)
)
.hovered_sync(hovered.clone())
.child(
El::<Text>::new()
.text_font(TextFont::from_font_size(font_size.get())) .text_font_signal(font_size.signal().map(TextFont::from_font_size))
.text_color_signal(
if blocked {
error_color.signal().apply(SignalEither::Left)
} else {
hovered.signal().map_bool_signal(
clone!((highlighted_color) move || highlighted_color.signal()),
clone!((unhighlighted_color) move || unhighlighted_color.signal())
)
.apply(SignalEither::Right)
}
.map(TextColor)
)
.text(Text(option.to_string()))
);
if blocked {
if let Some(text) = &*blocked_tooltip.clone() {
el = el
.update_raw_el(|raw_el| {
raw_el
.apply(sync_tooltip_position(expected_tooltip_height))
.on_signal_with_system(
hovered.signal(),
clone!((text) move |
In((entity, hovered)),
mut tooltip_cache: TooltipCache,
| {
if let Some(tooltip) = tooltip_cache.get(entity) {
let data = Some(TooltipData::new(entity, text.clone()));
let mut lock = tooltip.lock_mut();
if hovered {
if *lock != data {
*lock = data;
}
} else if *lock == data {
*lock = None;
}
}
})
)
});
}
} else {
el = el
.cursor(CursorIcon::System(SystemCursorIcon::Pointer));
if let Some(system) = option_handler_system {
el = el
.on_click_with_system(move |_: In<_>, mut commands: Commands| {
if let Some(i) = i.get() {
commands.run_system_with(system, i);
}
});
}
}
el
}))
)
}),
))
}
}
impl<T: Clone + PartialEq + Display + Send + Sync + 'static> GlobalEventAware for Dropdown<T> {}
impl<T: Clone + PartialEq + Display + Send + Sync + 'static> PointerEventAware for Dropdown<T> {}
impl<T> Dropdown<T> {
pub fn new(options: MutableVec<OptionData<T>>) -> Self
where
T: Clone + PartialEq + Display + Send + Sync + 'static,
{
Self {
el: El::<Node>::new(),
options,
option_handler_system: Arc::new(OnceLock::new()),
blocked_tooltip: None,
selected: Mutable::new(None),
show_dropdown: Mutable::new(false),
font_size: GLOBAL_FONT_SIZE.clone(),
padding: Mutable::new(GLOBAL_PADDING.get() * 0.5),
border_radius: GLOBAL_BORDER_RADIUS.clone(),
border_width: GLOBAL_BORDER_WIDTH.clone(),
background_color: GLOBAL_PRIMARY_BACKGROUND_COLOR.clone(),
highlighted_color: GLOBAL_HIGHLIGHTED_COLOR.clone(),
unhighlighted_color: GLOBAL_UNHIGHLIGHTED_COLOR.clone(),
border_color: GLOBAL_BORDER_COLOR.clone(),
error_color: GLOBAL_ERROR_COLOR.clone(),
}
}
impl_syncers! { selected: Option<usize>, show_dropdown: bool, font_size: f32, padding: f32, border_radius: f32, border_width: f32, background_color: Color, highlighted_color: Color, unhighlighted_color: Color, border_color: Color }
pub fn with_show_dropdown(mut self, show_dropdown: Mutable<bool>) -> Self {
self.show_dropdown = show_dropdown;
self
}
pub fn sync_selected(self, selected: Mutable<Option<usize>>) -> Self
where
Self: ElementWrapper,
{
let syncer = spawn(sync_neq(selected.signal(), self.selected.clone()));
self.update_raw_el(|raw_el| raw_el.hold_tasks([syncer]))
}
pub fn option_handler_system<Marker>(self, handler: impl IntoSystem<In<usize>, (), Marker> + Send + 'static) -> Self
where
Self: ElementWrapper,
{
let system_holder = self.option_handler_system.clone();
self.update_raw_el(|raw_el| {
raw_el
.with_entity(clone!((system_holder) move |mut entity| {
let handler = entity.world_scope(|world| register_system(world, handler));
let _ = system_holder.set(handler);
}))
.apply(remove_system_holder_on_remove(system_holder))
})
}
pub fn option_handler(self, mut option_handler: impl FnMut(usize) + Send + Sync + 'static) -> Self
where
Self: ElementWrapper,
{
self.option_handler_system(move |In(i)| option_handler(i))
}
pub fn basic_option_handler(self) -> Self
where
Self: ElementWrapper,
{
let f = clone!((self.selected => selected, self.show_dropdown => show_dropdown) move |i| {
selected.set(Some(i));
show_dropdown.set(false);
});
self.option_handler(f)
}
pub fn blocked_tooltip(mut self, text: String) -> Self {
self.blocked_tooltip = Some(text);
self
}
}