use crate::fyrox::{
core::{
make_pretty_type_name, parking_lot::Mutex, pool::Handle, reflect::prelude::*,
sstorage::ImmutableString, type_traits::prelude::*, uuid_provider, visitor::prelude::*,
},
fxhash::FxHashSet,
graph::SceneGraph,
gui::{
border::BorderBuilder,
button::{ButtonBuilder, ButtonMessage},
define_widget_deref,
grid::{Column, GridBuilder, Row},
message::{KeyCode, MessageDirection, UiMessage},
scroll_viewer::{ScrollViewerBuilder, ScrollViewerMessage},
searchbar::{SearchBarBuilder, SearchBarMessage},
stack_panel::StackPanelBuilder,
text::TextBuilder,
tree::{Tree, TreeBuilder, TreeRootBuilder, TreeRootMessage},
widget::{Widget, WidgetBuilder, WidgetMessage},
window::{Window, WindowBuilder, WindowMessage},
BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
},
};
use fyrox::gui::button::Button;
use fyrox::gui::control_trait_proxy_impls;
use fyrox::gui::message::MessageData;
use fyrox::gui::scroll_viewer::ScrollViewer;
use fyrox::gui::searchbar::SearchBar;
use fyrox::gui::style::resource::StyleResourceExt;
use fyrox::gui::style::Style;
use fyrox::gui::text_box::EmptyTextPlaceholder;
use fyrox::gui::tree::TreeRoot;
use std::{
any::TypeId,
ops::{Deref, DerefMut},
sync::Arc,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PropertySelectorMessage {
Selection(Vec<PropertyDescriptorData>),
ChooseFocus,
}
impl MessageData for PropertySelectorMessage {}
pub struct PropertyDescriptor {
path: String,
display_name: String,
type_name: String,
type_id: TypeId,
read_only: bool,
children_properties: Vec<PropertyDescriptor>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PropertyDescriptorData {
pub name: String,
pub path: String,
pub type_id: TypeId,
}
fn make_views_for_property_descriptor_collection(
ctx: &mut BuildContext,
collection: &[PropertyDescriptor],
allowed_types: Option<&FxHashSet<TypeId>>,
) -> Vec<Handle<Tree>> {
collection
.iter()
.filter_map(|p| {
let view = p.make_view(ctx, allowed_types);
if view.is_some() {
Some(view)
} else {
None
}
})
.collect()
}
fn apply_filter_recursive(node: Handle<UiNode>, filter: &str, ui: &UserInterface) -> bool {
let node_ref = ui.node(node);
let mut is_any_match = false;
for &child in node_ref.children() {
is_any_match |= apply_filter_recursive(child, filter, ui)
}
if let Some(data) = node_ref
.query_component::<Tree>()
.and_then(|n| n.user_data_cloned::<PropertyDescriptorData>())
{
is_any_match |= data.name.to_lowercase().contains(filter);
ui.send(node, WidgetMessage::Visibility(is_any_match));
}
is_any_match
}
impl PropertyDescriptor {
fn make_view(
&self,
ctx: &mut BuildContext,
allowed_types: Option<&FxHashSet<TypeId>>,
) -> Handle<Tree> {
if self.read_only {
return Handle::NONE;
}
let items = make_views_for_property_descriptor_collection(
ctx,
&self.children_properties,
allowed_types,
);
if !items.is_empty() || allowed_types.is_none_or(|types| types.contains(&self.type_id)) {
let name = format!(
"{} ({})",
self.display_name,
make_pretty_type_name(&self.type_name)
);
TreeBuilder::new(WidgetBuilder::new().with_user_data(Arc::new(Mutex::new(
PropertyDescriptorData {
name: name.clone(),
path: self.path.clone(),
type_id: self.type_id,
},
))))
.with_items(items)
.with_content(
TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0)))
.with_text(name)
.build(ctx),
)
.build(ctx)
} else {
Handle::NONE
}
}
}
pub fn object_to_property_tree<F>(
parent_path: &str,
object: &dyn Reflect,
filter: &mut F,
) -> Vec<PropertyDescriptor>
where
F: FnMut(&FieldRef) -> bool,
{
let mut descriptors = Vec::new();
object.fields_ref(&mut |fields_ref| {
for field_info in fields_ref.iter() {
if !filter(field_info) {
continue;
}
let field_ref = field_info.value.field_value_as_reflect();
let path = if parent_path.is_empty() {
field_info.name.to_owned()
} else {
format!("{}.{}", parent_path, field_info.name)
};
let mut processed = true;
field_ref.as_array(&mut |array| match array {
Some(array) => {
let mut descriptor = PropertyDescriptor {
path: path.clone(),
display_name: field_info.display_name.to_owned(),
type_name: field_info.value.type_name().to_owned(),
type_id: field_info.value.type_id(),
children_properties: Default::default(),
read_only: field_info.read_only,
};
for i in 0..array.reflect_len() {
if let Some(item) = array.reflect_index(i) {
let item_path = format!("{path}[{i}]");
descriptor.children_properties.push(PropertyDescriptor {
path: item_path.clone(),
display_name: format!("[{i}]"),
type_name: item.type_name().to_owned(),
type_id: item.type_id(),
read_only: field_info.read_only,
children_properties: object_to_property_tree(
&item_path, item, filter,
),
})
}
}
descriptors.push(descriptor);
}
None => {
processed = false;
}
});
if !processed {
field_ref.as_hash_map(&mut |result| match result {
Some(hash_map) => {
let mut descriptor = PropertyDescriptor {
path: path.clone(),
display_name: field_info.display_name.to_owned(),
type_name: field_info.value.type_name().to_owned(),
type_id: field_info.value.type_id(),
children_properties: Default::default(),
read_only: field_info.read_only,
};
for i in 0..hash_map.reflect_len() {
let (key, value) = hash_map.reflect_get_at(i).unwrap();
let mut key_str = format!("{key:?}");
let mut is_key_string = false;
key.downcast_ref::<String>(&mut |string| {
is_key_string |= string.is_some()
});
key.downcast_ref::<ImmutableString>(&mut |string| {
is_key_string |= string.is_some()
});
if is_key_string {
key_str.remove(0);
key_str.pop();
}
let item_path = format!("{path}[{key_str}]");
descriptor.children_properties.push(PropertyDescriptor {
path: item_path.clone(),
display_name: format!("[{key_str}]"),
type_name: value.type_name().to_owned(),
type_id: value.type_id(),
read_only: field_info.read_only,
children_properties: object_to_property_tree(
&item_path, value, filter,
),
})
}
descriptors.push(descriptor);
processed = true;
}
None => {
processed = false;
}
})
}
if !processed {
descriptors.push(PropertyDescriptor {
display_name: field_info.display_name.to_owned(),
type_name: field_info.value.type_name().to_owned(),
type_id: field_info.value.type_id(),
read_only: field_info.read_only,
children_properties: object_to_property_tree(&path, field_ref, filter),
path: path.clone(),
})
}
}
});
descriptors
}
#[derive(Clone, Visit, Reflect, Debug, ComponentProvider)]
#[reflect(derived_type = "UiNode")]
pub struct PropertySelector {
widget: Widget,
#[reflect(hidden)]
#[visit(skip)]
selected_property_paths: Vec<PropertyDescriptorData>,
tree_root: Handle<TreeRoot>,
search_bar: Handle<SearchBar>,
scroll_viewer: Handle<ScrollViewer>,
}
define_widget_deref!(PropertySelector);
uuid_provider!(PropertySelector = "8e58e123-48a1-4e18-9e90-fd35a1669bdc");
impl PropertySelector {
fn find_selected_tree_items(&self, ui: &UserInterface) -> Vec<Handle<Tree>> {
let mut stack = vec![self.tree_root.to_base()];
let mut selected_trees = Vec::new();
while let Some(node_handle) = stack.pop() {
let node = ui.node(node_handle);
if let Some(tree) = node.query_component::<Tree>() {
if self.selected_property_paths.iter().any(|path| {
path.path
== tree
.user_data_cloned::<PropertyDescriptorData>()
.unwrap()
.path
}) {
selected_trees.push(node_handle.to_variant());
}
}
stack.extend_from_slice(node.children());
}
selected_trees
}
fn sync_selection(&self, ui: &UserInterface) {
let selected_trees = self.find_selected_tree_items(ui);
if let Some(first) = selected_trees.first() {
ui.send(
self.scroll_viewer,
ScrollViewerMessage::BringIntoView(first.to_base()),
)
}
ui.send(self.tree_root, TreeRootMessage::Select(selected_trees));
}
}
impl Control for PropertySelector {
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.widget.handle_routed_message(ui, message);
if let Some(TreeRootMessage::Select(selection)) = message.data_from(self.tree_root) {
ui.send(
self.handle,
PropertySelectorMessage::Selection(
selection
.iter()
.map(|s| {
ui[*s]
.user_data_cloned::<PropertyDescriptorData>()
.unwrap()
.clone()
})
.collect(),
),
);
} else if let Some(msg) = message.data_for::<PropertySelectorMessage>(self.handle) {
match msg {
PropertySelectorMessage::Selection(selection) => {
if &self.selected_property_paths != selection {
self.selected_property_paths.clone_from(selection);
ui.send_message(message.reverse());
}
}
PropertySelectorMessage::ChooseFocus => {
ui.send(self.search_bar, WidgetMessage::Focus);
self.sync_selection(ui);
}
}
} else if let Some(SearchBarMessage::Text(filter_text)) = message.data_from(self.search_bar)
{
apply_filter_recursive(self.tree_root.to_base(), &filter_text.to_lowercase(), ui);
}
}
}
pub struct PropertySelectorBuilder {
widget_builder: WidgetBuilder,
property_descriptors: Vec<PropertyDescriptor>,
allowed_types: Option<FxHashSet<TypeId>>,
selected_property_paths: Vec<PropertyDescriptorData>,
}
impl PropertySelectorBuilder {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self {
widget_builder,
property_descriptors: Default::default(),
allowed_types: Default::default(),
selected_property_paths: Default::default(),
}
}
pub fn with_allowed_types(mut self, allowed_types: Option<FxHashSet<TypeId>>) -> Self {
self.allowed_types = allowed_types;
self
}
pub fn with_property_descriptors(mut self, descriptors: Vec<PropertyDescriptor>) -> Self {
self.property_descriptors = descriptors;
self
}
pub fn with_selected_property_paths(mut self, paths: Vec<PropertyDescriptorData>) -> Self {
self.selected_property_paths = paths;
self
}
pub fn build(self, ctx: &mut BuildContext) -> Handle<PropertySelector> {
let tree_root;
let search_bar;
let scroll_viewer =
ScrollViewerBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0)))
.with_content({
tree_root = TreeRootBuilder::new(WidgetBuilder::new().with_tab_index(Some(1)))
.with_items(make_views_for_property_descriptor_collection(
ctx,
&self.property_descriptors,
self.allowed_types.as_ref(),
))
.build(ctx);
tree_root
})
.build(ctx);
let content = GridBuilder::new(
WidgetBuilder::new()
.with_child({
search_bar = SearchBarBuilder::new(
WidgetBuilder::new()
.on_row(0)
.on_column(0)
.with_margin(Thickness::uniform(1.0))
.with_tab_index(Some(0)),
)
.with_empty_text_placeholder(EmptyTextPlaceholder::Text(
"Search for a property",
))
.build(ctx);
search_bar
})
.with_child(
BorderBuilder::new(
WidgetBuilder::new()
.with_background(ctx.style.property(Style::BRUSH_DARK))
.on_row(1)
.on_column(0)
.with_child(scroll_viewer),
)
.build(ctx),
),
)
.add_row(Row::strict(22.0))
.add_row(Row::stretch())
.add_column(Column::stretch())
.build(ctx);
let selector = PropertySelector {
widget: self.widget_builder.with_child(content).build(ctx),
selected_property_paths: self.selected_property_paths,
tree_root,
search_bar,
scroll_viewer,
};
ctx.add(selector)
}
}
#[derive(Clone, Visit, Reflect, Debug, ComponentProvider)]
#[reflect(derived_type = "UiNode")]
pub struct PropertySelectorWindow {
#[component(include)]
window: Window,
selector: Handle<PropertySelector>,
ok: Handle<Button>,
cancel: Handle<Button>,
#[reflect(hidden)]
#[visit(skip)]
allowed_types: Option<FxHashSet<TypeId>>,
}
impl Deref for PropertySelectorWindow {
type Target = Widget;
fn deref(&self) -> &Self::Target {
&self.window.widget
}
}
impl DerefMut for PropertySelectorWindow {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.window.widget
}
}
impl PropertySelectorWindow {
pub fn confirm(&self, ui: &UserInterface) {
ui.post(
self.handle,
PropertySelectorMessage::Selection(ui[self.selector].selected_property_paths.clone()),
);
ui.send(self.handle, WindowMessage::Close);
}
}
uuid_provider!(PropertySelectorWindow = "725e4a10-eca6-4345-9833-d54dae2f20f2");
impl Control for PropertySelectorWindow {
control_trait_proxy_impls!(window);
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.window.handle_routed_message(ui, message);
if let Some(ButtonMessage::Click) = message.data() {
if message.destination() == self.ok {
self.confirm(ui);
} else if message.destination() == self.cancel {
ui.send(self.handle, WindowMessage::Close);
}
} else if let Some(PropertySelectorMessage::Selection(selection)) = message.data() {
if message.destination() == self.selector
&& message.direction() == MessageDirection::FromWidget
{
let enabled = selection.iter().all(|d| {
self.allowed_types
.as_ref()
.is_none_or(|types| types.contains(&d.type_id))
});
ui.send(self.ok, WidgetMessage::Enabled(enabled));
}
} else if let Some(WindowMessage::Open { .. }) = message.data() {
ui.send(self.selector, PropertySelectorMessage::ChooseFocus);
} else if let Some(WidgetMessage::KeyDown(KeyCode::Enter | KeyCode::NumpadEnter)) =
message.data()
{
if !message.handled() {
self.confirm(ui);
message.set_handled(true);
}
}
}
}
pub struct PropertySelectorWindowBuilder {
window_builder: WindowBuilder,
property_descriptors: Vec<PropertyDescriptor>,
allowed_types: Option<FxHashSet<TypeId>>,
selected_property_paths: Vec<PropertyDescriptorData>,
}
impl PropertySelectorWindowBuilder {
pub fn new(window_builder: WindowBuilder) -> Self {
Self {
window_builder,
property_descriptors: Default::default(),
allowed_types: None,
selected_property_paths: Default::default(),
}
}
pub fn with_allowed_types(mut self, allowed_types: Option<FxHashSet<TypeId>>) -> Self {
self.allowed_types = allowed_types;
self
}
pub fn with_property_descriptors(mut self, descriptors: Vec<PropertyDescriptor>) -> Self {
self.property_descriptors = descriptors;
self
}
pub fn with_selected_property_paths(mut self, paths: Vec<PropertyDescriptorData>) -> Self {
self.selected_property_paths = paths;
self
}
pub fn build(self, ctx: &mut BuildContext) -> Handle<PropertySelectorWindow> {
let selector;
let ok;
let cancel;
let content = GridBuilder::new(
WidgetBuilder::new()
.with_child({
selector = PropertySelectorBuilder::new(
WidgetBuilder::new()
.on_row(0)
.on_column(0)
.with_margin(Thickness::uniform(1.0)),
)
.with_selected_property_paths(self.selected_property_paths)
.with_allowed_types(self.allowed_types.clone())
.with_property_descriptors(self.property_descriptors)
.build(ctx);
selector
})
.with_child(
StackPanelBuilder::new(
WidgetBuilder::new()
.with_horizontal_alignment(HorizontalAlignment::Right)
.on_row(1)
.on_column(0)
.with_margin(Thickness::uniform(2.0))
.with_child({
ok = ButtonBuilder::new(
WidgetBuilder::new()
.with_enabled(false)
.with_width(100.0)
.with_margin(Thickness::uniform(1.0))
.with_tab_index(Some(2)),
)
.with_text("OK")
.build(ctx);
ok
})
.with_child({
cancel = ButtonBuilder::new(
WidgetBuilder::new()
.with_width(100.0)
.with_margin(Thickness::uniform(1.0))
.with_tab_index(Some(3)),
)
.with_text("Cancel")
.build(ctx);
cancel
}),
)
.with_orientation(Orientation::Horizontal)
.build(ctx),
),
)
.add_row(Row::stretch())
.add_row(Row::strict(26.0))
.add_column(Column::stretch())
.build(ctx);
let window = PropertySelectorWindow {
window: self.window_builder.with_content(content).build_window(ctx),
selector,
ok,
cancel,
allowed_types: self.allowed_types,
};
ctx.add(window)
}
}
#[cfg(test)]
mod test {
use crate::scene::property::{PropertySelectorBuilder, PropertySelectorWindowBuilder};
use fyrox::gui::window::WindowBuilder;
use fyrox::{gui::test::test_widget_deletion, gui::widget::WidgetBuilder};
#[test]
fn test_deletion() {
test_widget_deletion(|ctx| PropertySelectorBuilder::new(WidgetBuilder::new()).build(ctx));
test_widget_deletion(|ctx| {
PropertySelectorWindowBuilder::new(WindowBuilder::new(WidgetBuilder::new())).build(ctx)
});
}
}