use crate::{
asset::{item::AssetItemMessage, preview::cache::IconRequest},
fyrox::{
asset::{manager::ResourceManager, untyped::UntypedResource, Resource, TypedResourceData},
core::{
log::Log,
pool::{Handle, HandlesVecExtension},
reflect::prelude::*,
some_or_continue,
type_traits::prelude::*,
visitor::prelude::*,
PhantomDataSendSync, SafeLock,
},
graph::SceneGraph,
gui::{
border::BorderBuilder,
brush::Brush,
button::{Button, ButtonBuilder, ButtonMessage},
control_trait_proxy_impls,
decorator::DecoratorBuilder,
define_widget_deref,
formatted_text::WrapMode,
grid::{Column, GridBuilder, Row},
image::Image,
image::{ImageBuilder, ImageMessage},
list_view::{ListView, ListViewBuilder, ListViewMessage},
message::{MessageData, UiMessage},
searchbar::{SearchBar, SearchBarBuilder, SearchBarMessage},
stack_panel::StackPanelBuilder,
text::{Text, TextBuilder},
text_box::EmptyTextPlaceholder,
widget::{Widget, WidgetBuilder, WidgetMessage},
window::WindowAlignment,
window::{Window, WindowBuilder, WindowMessage, WindowTitle},
wrap_panel::WrapPanelBuilder,
BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode,
UserInterface, VerticalAlignment,
},
},
};
use fyrox::gui::style::resource::StyleResourceExt;
use fyrox::gui::style::Style;
use rust_fuzzy_search::fuzzy_compare;
use std::{
borrow::Cow,
cell::Cell,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
sync::mpsc::Sender,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssetSelectorMessage {
Select(UntypedResource),
}
impl MessageData for AssetSelectorMessage {}
#[derive(Clone, Debug, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
#[type_uuid(id = "aa4f0726-8d25-4c90-add1-92ba392310c6")]
struct Item {
pub widget: Widget,
image: Handle<Image>,
path: PathBuf,
#[visit(skip)]
#[reflect(hidden)]
sender: Sender<IconRequest>,
#[visit(skip)]
#[reflect(hidden)]
need_request_preview: Cell<bool>,
#[visit(skip)]
#[reflect(hidden)]
resource_manager: ResourceManager,
}
define_widget_deref!(Item);
impl Control for Item {
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.widget.handle_routed_message(ui, message);
if message.destination() == self.handle {
if let Some(AssetItemMessage::Icon {
texture,
flip_y,
color,
}) = message.data()
{
ui.send(self.image, ImageMessage::Texture(texture.clone()));
ui.send(self.image, ImageMessage::Flip(*flip_y));
ui.send(
self.image,
WidgetMessage::Background(Brush::Solid(*color).into()),
)
}
}
}
fn update(&mut self, _dt: f32, ui: &mut UserInterface) {
if self.need_request_preview.get() {
let parent_container_bounds = ui
.find_up(self.parent, &mut |n| n.has_component::<ListView>())
.map(|(_, node)| node.screen_bounds())
.unwrap_or_default();
let screen_bounds = self.screen_bounds();
for corner in [
screen_bounds.left_top_corner(),
screen_bounds.right_top_corner(),
screen_bounds.right_bottom_corner(),
screen_bounds.left_bottom_corner(),
] {
if parent_container_bounds.contains(corner) {
self.need_request_preview.set(false);
Log::verify(self.sender.send(IconRequest {
widget_handle: self.handle,
resource: self.resource_manager.request_untyped(self.path.as_path()),
force_update: false,
}));
break;
}
}
}
}
}
struct ItemBuilder {
widget_builder: WidgetBuilder,
path: PathBuf,
sender: Sender<IconRequest>,
}
impl ItemBuilder {
fn new(sender: Sender<IconRequest>, widget_builder: WidgetBuilder) -> Self {
Self {
widget_builder,
path: Default::default(),
sender,
}
}
pub fn with_path(mut self, path: PathBuf) -> Self {
self.path = path;
self
}
fn build(self, resource_manager: ResourceManager, ctx: &mut BuildContext) -> Handle<Item> {
let image = ImageBuilder::new(
WidgetBuilder::new()
.with_vertical_alignment(VerticalAlignment::Top)
.with_height(64.0)
.with_width(64.0)
.with_margin(Thickness::uniform(1.0))
.on_row(0),
)
.build(ctx);
let content = GridBuilder::new(
WidgetBuilder::new().with_child(image).with_child(
TextBuilder::new(
WidgetBuilder::new()
.on_row(1)
.with_width(64.0)
.with_margin(Thickness::uniform(1.0)),
)
.with_vertical_text_alignment(VerticalAlignment::Top)
.with_horizontal_text_alignment(HorizontalAlignment::Center)
.with_wrap(WrapMode::Word)
.with_text(
self.path
.file_name()
.map(|file_name| file_name.to_string_lossy().to_string())
.unwrap_or_default(),
)
.build(ctx),
),
)
.add_row(Row::auto())
.add_row(Row::auto())
.add_column(Column::auto())
.build(ctx);
let item = Item {
widget: self
.widget_builder
.with_need_update(true)
.with_child(
DecoratorBuilder::new(
BorderBuilder::new(WidgetBuilder::new().with_child(content))
.with_pad_by_corner_radius(false)
.with_stroke_thickness(Thickness::uniform(2.0).into())
.with_corner_radius(4.0.into()),
)
.with_selected_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
.build(ctx),
)
.build(ctx),
image,
path: self.path,
sender: self.sender,
need_request_preview: Cell::new(true),
resource_manager,
};
ctx.add(item)
}
}
#[derive(Clone, Debug, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
#[type_uuid(id = "970bb83b-51e8-48e7-8050-f97bf0ac470b")]
pub struct AssetSelector {
pub widget: Widget,
list_view: Handle<ListView>,
resources: Vec<PathBuf>,
#[visit(skip)]
#[reflect(hidden)]
resource_manager: ResourceManager,
}
define_widget_deref!(AssetSelector);
impl Control for AssetSelector {
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.widget.handle_routed_message(ui, message);
if let Some(ListViewMessage::Selection(selected)) = message.data_from(self.list_view) {
if let Some(path) = selected.first().and_then(|i| self.resources.get(*i)) {
let resource = self.resource_manager.request_untyped(path);
ui.post(self.handle(), AssetSelectorMessage::Select(resource));
}
} else if let Some(WidgetMessage::Focus) = message.data_for(self.handle) {
ui.send(self.list_view, WidgetMessage::Focus);
}
}
}
pub struct AssetSelectorBuilder<'a> {
widget_builder: WidgetBuilder,
asset_types: Vec<Uuid>,
selected_asset_path: Cow<'a, Path>,
}
impl<'a> AssetSelectorBuilder<'a> {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self {
widget_builder,
asset_types: Default::default(),
selected_asset_path: Default::default(),
}
}
pub fn with_asset_types(mut self, asset_types: Vec<Uuid>) -> Self {
self.asset_types = asset_types;
self
}
pub fn with_selected_asset_path(mut self, selected_asset_path: Cow<'a, Path>) -> Self {
self.selected_asset_path = selected_asset_path;
self
}
pub fn build(
self,
icon_request_sender: Sender<IconRequest>,
resource_manager: ResourceManager,
ctx: &mut BuildContext,
) -> Handle<AssetSelector> {
let state = resource_manager.state();
let loaders = state.loaders.safe_lock();
let registry = state.resource_registry.safe_lock();
let mut supported_resource_paths = loaders
.iter()
.filter_map(|loader| {
if self.asset_types.contains(&loader.data_type_uuid()) {
Some(
registry
.inner()
.values()
.filter(|path| {
if let Some(ext) = path.extension().map(|ext| ext.to_string_lossy())
{
loader.supports_extension(&ext)
} else {
false
}
})
.cloned()
.collect::<Vec<_>>(),
)
} else {
None
}
})
.flatten()
.collect::<Vec<_>>();
supported_resource_paths.extend(state.built_in_resources.values().filter_map(|res| {
let resource_state = res.resource.lock();
if self
.asset_types
.contains(&resource_state.state.data_ref()?.type_uuid())
{
Some(res.id.clone())
} else {
None
}
}));
supported_resource_paths.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
let selection_index = supported_resource_paths
.iter()
.enumerate()
.find(|(_, path)| path.as_path() == self.selected_asset_path)
.map(|(i, _)| i);
let items = supported_resource_paths
.iter()
.map(|path| {
ItemBuilder::new(
icon_request_sender.clone(),
WidgetBuilder::new().with_margin(Thickness::uniform(2.0)),
)
.with_path(path.clone())
.build(resource_manager.clone(), ctx)
})
.collect::<Vec<_>>();
let selected_item = selection_index.and_then(|i| items.get(i).cloned());
let list_view = ListViewBuilder::new(WidgetBuilder::new())
.with_items_panel(
WrapPanelBuilder::new(
WidgetBuilder::new()
.with_vertical_alignment(VerticalAlignment::Top)
.with_horizontal_alignment(HorizontalAlignment::Stretch),
)
.with_orientation(Orientation::Horizontal)
.build(ctx),
)
.with_selection(selection_index.map(|i| vec![i]).unwrap_or_default())
.with_items(items.to_base())
.build(ctx);
if let Some(selected_item) = selected_item {
ctx.inner().send(
list_view,
ListViewMessage::BringItemIntoView(selected_item.to_base()),
);
}
let selector = AssetSelector {
widget: self.widget_builder.with_child(list_view).build(ctx),
list_view: list_view.transmute(),
resources: supported_resource_paths,
resource_manager: resource_manager.clone(),
};
ctx.add(selector)
}
}
#[derive(Clone, Debug, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
#[type_uuid(id = "c348ad3d-52a6-40ad-a5e4-bf63fefe1906")]
pub struct AssetSelectorWindow {
pub window: Window,
selector: Handle<AssetSelector>,
ok: Handle<Button>,
cancel: Handle<Button>,
selected_resource: Option<UntypedResource>,
search_bar: Handle<SearchBar>,
}
impl Deref for AssetSelectorWindow {
type Target = Widget;
fn deref(&self) -> &Self::Target {
&self.window.widget
}
}
impl DerefMut for AssetSelectorWindow {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.window.widget
}
}
impl Control for AssetSelectorWindow {
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(AssetSelectorMessage::Select(resource)) = message.data_from(self.selector) {
self.selected_resource = Some(resource.clone());
} else if let Some(ButtonMessage::Click) = message.data_from(self.ok) {
if let Some(resource) = self.selected_resource.as_ref().cloned() {
ui.post(self.handle, AssetSelectorMessage::Select(resource));
}
ui.send(self.handle, WindowMessage::Close);
} else if let Some(ButtonMessage::Click) = message.data_from(self.cancel) {
ui.send(self.handle, WindowMessage::Close);
} else if let Some(WindowMessage::Open { .. }) = message.data_for(self.handle) {
ui.send(self.search_bar, WidgetMessage::Focus);
} else if let Some(SearchBarMessage::Text(text)) = message.data_from(self.search_bar) {
let selector = &ui[self.selector];
let items = &*ui[selector.list_view].items;
let filter_text = text.to_lowercase();
for item in items {
let mut matches = false;
for (_, widget) in ui.traverse_iter(*item) {
let text_widget = some_or_continue!(widget.cast::<Text>());
let text = text_widget.text().to_lowercase();
matches |= text.contains(&filter_text)
|| fuzzy_compare(&filter_text, text.as_str()) >= 0.5;
}
let is_visible = matches || filter_text.is_empty();
ui.send(*item, WidgetMessage::Visibility(is_visible));
}
}
}
}
pub struct AssetSelectorWindowBuilder<'a> {
window_builder: WindowBuilder,
asset_types: Vec<Uuid>,
selected_asset_path: Cow<'a, Path>,
}
impl<'a> AssetSelectorWindowBuilder<'a> {
pub fn new(window_builder: WindowBuilder) -> Self {
Self {
window_builder,
asset_types: Default::default(),
selected_asset_path: Default::default(),
}
}
pub fn with_asset_types(mut self, asset_types: Vec<Uuid>) -> Self {
self.asset_types = asset_types;
self
}
pub fn with_selected_asset_path(mut self, selected_asset_path: Cow<'a, Path>) -> Self {
self.selected_asset_path = selected_asset_path;
self
}
pub fn build(
self,
sender: Sender<IconRequest>,
resource_manager: ResourceManager,
ctx: &mut BuildContext,
) -> Handle<AssetSelectorWindow> {
let search_bar = SearchBarBuilder::new(
WidgetBuilder::new()
.on_row(0)
.with_margin(Thickness::uniform(2.0))
.with_height(22.0)
.with_tab_index(Some(0)),
)
.with_empty_text_placeholder(EmptyTextPlaceholder::Text("Search for an asset"))
.build(ctx);
let selector =
AssetSelectorBuilder::new(WidgetBuilder::new().on_row(1).with_tab_index(Some(1)))
.with_selected_asset_path(self.selected_asset_path)
.with_asset_types(self.asset_types)
.build(sender, resource_manager, ctx);
let ok;
let cancel;
let buttons = StackPanelBuilder::new(
WidgetBuilder::new()
.on_row(2)
.with_horizontal_alignment(HorizontalAlignment::Right)
.with_margin(Thickness::uniform(2.0))
.with_child({
ok = ButtonBuilder::new(
WidgetBuilder::new()
.with_width(100.0)
.with_height(22.0)
.with_margin(Thickness::uniform(1.0))
.with_tab_index(Some(2)),
)
.with_text("Select")
.build(ctx);
ok
})
.with_child({
cancel = ButtonBuilder::new(
WidgetBuilder::new()
.with_width(100.0)
.with_height(22.0)
.with_margin(Thickness::uniform(1.0))
.with_tab_index(Some(3)),
)
.with_text("Cancel")
.build(ctx);
cancel
}),
)
.with_orientation(Orientation::Horizontal)
.build(ctx);
let content = GridBuilder::new(
WidgetBuilder::new()
.with_child(search_bar)
.with_child(selector)
.with_child(buttons),
)
.add_row(Row::auto())
.add_row(Row::stretch())
.add_row(Row::auto())
.add_column(Column::stretch())
.build(ctx);
let window = AssetSelectorWindow {
window: self.window_builder.with_content(content).build_window(ctx),
selector: selector.transmute(),
ok: ok.transmute(),
cancel: cancel.transmute(),
selected_resource: None,
search_bar: search_bar.transmute(),
};
ctx.add(window)
}
pub fn build_for_type_and_open<T: TypedResourceData>(
resource: Option<&Resource<T>>,
icon_request_sender: Sender<IconRequest>,
resource_manager: ResourceManager,
ui: &mut UserInterface,
) -> Handle<AssetSelectorWindow> {
let selector = AssetSelectorWindowBuilder::new(
WindowBuilder::new(WidgetBuilder::new().with_width(300.0).with_height(400.0))
.with_title(WindowTitle::text("Select a Resource"))
.with_remove_on_close(true)
.open(false),
)
.with_selected_asset_path(
resource
.and_then(|resource| resource_manager.resource_path(resource))
.unwrap_or_default()
.into(),
)
.with_asset_types(vec![<T as TypeUuidProvider>::type_uuid()])
.build(icon_request_sender, resource_manager, &mut ui.build_ctx());
ui.send(
selector,
WindowMessage::Open {
alignment: WindowAlignment::Center,
modal: true,
focus_content: false,
},
);
selector
}
}
#[derive(Reflect, Visit, Debug)]
pub struct AssetSelectorMixin<T: TypedResourceData> {
pub selector: Cell<Handle<AssetSelectorWindow>>,
pub select: Handle<Button>,
#[visit(skip)]
#[reflect(hidden)]
pub icon_request_sender: Sender<IconRequest>,
#[visit(skip)]
#[reflect(hidden)]
pub resource_manager: ResourceManager,
#[visit(skip)]
#[reflect(hidden)]
pub phantom_data_send_sync: PhantomDataSendSync<T>,
}
impl<T: TypedResourceData> Clone for AssetSelectorMixin<T> {
fn clone(&self) -> Self {
Self {
selector: self.selector.clone(),
select: self.select,
icon_request_sender: self.icon_request_sender.clone(),
resource_manager: self.resource_manager.clone(),
phantom_data_send_sync: self.phantom_data_send_sync,
}
}
}
impl<T: TypedResourceData> AssetSelectorMixin<T> {
pub fn new(
select: Handle<Button>,
icon_request_sender: Sender<IconRequest>,
resource_manager: ResourceManager,
) -> Self {
Self {
selector: Default::default(),
select,
icon_request_sender,
resource_manager,
phantom_data_send_sync: Default::default(),
}
}
pub fn handle_ui_message(
&self,
resource: Option<&Resource<T>>,
ui: &mut UserInterface,
message: &UiMessage,
) {
if let Some(ButtonMessage::Click) = message.data() {
if message.destination() == self.select {
self.selector
.set(AssetSelectorWindowBuilder::build_for_type_and_open::<T>(
resource,
self.icon_request_sender.clone(),
self.resource_manager.clone(),
ui,
));
}
}
}
pub fn preview_ui_message<F>(&self, ui: &UserInterface, message: &mut UiMessage, converter: F)
where
F: FnOnce(&UntypedResource) -> UiMessage,
{
if message.destination() == self.selector.get() {
if let Some(WindowMessage::Close) = message.data() {
self.selector.set(Handle::NONE);
} else if let Some(AssetSelectorMessage::Select(resource)) = message.data() {
ui.send_message(converter(resource))
}
}
}
pub fn request_preview(&self, widget_handle: Handle<UiNode>, resource: &Resource<T>) {
self.icon_request_sender
.send(IconRequest {
widget_handle,
resource: resource.clone().into_untyped(),
force_update: false,
})
.unwrap()
}
}