#![allow(unused)]
use std::cell::Cell;
use std::rc::Rc;
use crate::{EventBlocker, Group, Path, Rectangle, Scroller, Stacker, Text};
use crate::{TextStyle, Textbox};
use pax_engine::api::{Click, Event, MouseDown, Store, Stroke, TextboxChange};
use pax_engine::api::{Color, Property};
use pax_engine::*;
use pax_runtime::api::NodeContext;
#[pax]
#[engine_import_path("pax_engine")]
#[inlined(
if _options_visible {
<Scroller
anchor_y=0%
y={100%+1px}
height=500%
width={100% + 8px}
x=1px
scroll_height={(Math::len(self._filtered_options)*100/5)%}
unclippable=true
>
<Stacker gutter=1px>
for option_data in self._filtered_options {
<ComboBoxListItem style=style background=background data={option_data} @new_item=self.dispatch_new_item/>
}
</Stacker>
<Rectangle fill={stroke.color}/>
</Scroller>
}
if self.text != "" && self.selected != None && !_options_visible {
<Group @click=self.remove_index x=100% width=30px>
<EventBlocker/>
<Path class=x_symbol x=50% y=50% width=15px height=15px/>
</Group>
}
<Textbox
@click=on_click
text=bind:text
style=style
background=background
stroke=stroke
border_radius=border_radius
@textbox_change=self.textbox_change
/>
@settings {
@mount: on_mount
@pre_render: update
.x_symbol {
elements: {[
PathElement::Point(0%, 10%),
PathElement::Line,
PathElement::Point(50% - 10%, 50%),
PathElement::Line,
PathElement::Point(0%, 100% - 10%),
PathElement::Line,
PathElement::Point(10%, 100%),
PathElement::Line,
PathElement::Point(50%, 50% + 10%),
PathElement::Line,
PathElement::Point(100% - 10%, 100%),
PathElement::Line,
PathElement::Point(100%, 100% - 10%),
PathElement::Line,
PathElement::Point(50% + 10%, 50%),
PathElement::Line,
PathElement::Point(100%, 10%),
PathElement::Line,
PathElement::Point(100% - 10%, 0%),
PathElement::Line,
PathElement::Point(50%, 50% - 10%),
PathElement::Line,
PathElement::Point(10%, 0%),
PathElement::Close
]},
stroke: {
width: 0
},
fill: rgb(200, 200, 200)
}
}
)]
pub struct ComboBox {
pub text: Property<String>,
pub selected: Property<Option<usize>>,
pub options: Property<Vec<String>>,
pub new_item: Property<NewItem>,
pub background: Property<Color>,
pub stroke: Property<Stroke>,
pub style: Property<TextStyle>,
pub border_radius: Property<f64>,
pub _filtered_options: Property<Vec<ListItemData>>,
pub _options_visible: Property<bool>,
pub _selected_listener: Property<bool>,
}
#[pax]
#[engine_import_path("pax_engine")]
pub enum NewItem {
#[default]
Disallow,
AllowInvalid,
Text(String),
}
const ZERO_WIDTH_SPACE: &str = "\u{200B}";
struct SelectedIndProp(Property<Option<usize>>);
impl Store for SelectedIndProp {}
impl ComboBox {
pub fn on_mount(&mut self, ctx: &NodeContext) {
ctx.push_local_store(SelectedIndProp(self.selected.clone()));
let options = self.options.clone();
let text = self.text.clone();
let deps = [options.untyped(), text.untyped()];
let new_item = self.new_item.clone();
self._filtered_options.replace_with(Property::computed(
move || {
options.read(|options| {
text.read(|text| {
let mut options_sorted: Vec<(usize, &String)> =
options.iter().enumerate().collect();
options_sorted.sort_by_key(|&(_, v)| v);
let mut filtered_options: Vec<_> = options_sorted
.into_iter()
.filter(|(_, t)| t.contains(text))
.map(|(i, v)| ListItemData {
text: v.clone(),
event: ComboBoxItemClickEvent::SelectIndex(i),
})
.collect();
filtered_options.sort_by_key(|v| v.text.starts_with(text));
if filtered_options.is_empty() {
match new_item.get() {
NewItem::Disallow => filtered_options.push(ListItemData {
text: String::from("No Results Found"),
event: ComboBoxItemClickEvent::None,
}),
NewItem::Text(text) => filtered_options.push(ListItemData {
text,
event: ComboBoxItemClickEvent::NewItem,
}),
NewItem::AllowInvalid => (),
}
}
filtered_options
})
})
},
&deps,
));
let text = self.text.clone();
let options = self.options.clone();
let options_visible = self._options_visible.clone();
let new_item_behavior = self.new_item.clone();
let selected = self.selected.clone();
let deps = [selected.untyped()];
let last = Rc::new(Cell::new(None));
self._selected_listener.replace_with(Property::computed(
move || {
let selected = selected.get();
let new_value = if let Some(selected) = selected {
options.read(|options| {
let index = selected.clamp(0, options.len());
options[index].clone()
})
} else {
"".to_string()
};
match new_item_behavior.get() {
NewItem::AllowInvalid => {
if last.get() != selected || text.get().is_empty() {
text.set(new_value)
}
}
_ => text.set(new_value),
}
options_visible.set(false);
last.set(selected);
true
},
&deps,
));
}
pub fn on_click(&mut self, ctx: &NodeContext, _event: Event<Click>) {
self._options_visible.set(true);
}
pub fn textbox_change(&mut self, ctx: &NodeContext, _event: Event<TextboxChange>) {
self.selected.set(self.selected.get());
}
pub fn remove_index(&mut self, ctx: &NodeContext, event: Event<Click>) {
self.selected.set(None);
self.text.set("".to_string());
}
pub fn update(&mut self, ctx: &NodeContext) {
self._selected_listener.get();
}
pub fn dispatch_new_item(&mut self, ctx: &NodeContext) {
ctx.dispatch_event("new_item").unwrap();
}
}
#[pax]
#[engine_import_path("pax_engine")]
#[inlined(
<Text x=3px text={data.text} width={100%-3px} height=100% style=style/>
<Rectangle fill=background/>
@settings {
@mouse_down: on_click
}
)]
pub struct ComboBoxListItem {
pub data: Property<ListItemData>,
pub background: Property<Color>,
pub style: Property<TextStyle>,
}
impl ComboBoxListItem {
pub fn on_click(&mut self, ctx: &NodeContext, _event: Event<MouseDown>) {
match self.data.get().event {
ComboBoxItemClickEvent::None => (),
ComboBoxItemClickEvent::SelectIndex(index) => {
let _ = ctx.peek_local_store(|SelectedIndProp(selected): &mut SelectedIndProp| {
selected.set(Some(index));
});
}
ComboBoxItemClickEvent::NewItem => {
ctx.dispatch_event("new_item");
}
}
}
}
#[pax]
#[engine_import_path("pax_engine")]
pub struct ListItemData {
pub text: String,
pub event: ComboBoxItemClickEvent,
}
#[pax]
#[engine_import_path("pax_engine")]
pub enum ComboBoxItemClickEvent {
#[default]
None,
SelectIndex(usize),
NewItem,
}