use crate::{
asset::item::AssetItem,
gui::make_dropdown_list_option,
preview::PreviewPanel,
scene::commands::material::{SetMaterialPropertyValueCommand, SetMaterialShaderCommand},
send_sync_message, GameEngine, Message,
};
use fyrox::{
core::{
algebra::{Matrix4, Vector2, Vector3, Vector4},
futures::executor::block_on,
make_relative_path,
parking_lot::Mutex,
pool::Handle,
sstorage::ImmutableString,
BiDirHashMap,
},
engine::resource_manager::ResourceManager,
gui::{
border::BorderBuilder,
check_box::{CheckBoxBuilder, CheckBoxMessage},
color::{ColorFieldBuilder, ColorFieldMessage},
dropdown_list::{DropdownListBuilder, DropdownListMessage},
grid::{Column, GridBuilder, Row},
image::{Image, ImageBuilder, ImageMessage},
list_view::{ListViewBuilder, ListViewMessage},
menu::{MenuItemBuilder, MenuItemContent, MenuItemMessage},
message::{MessageDirection, UiMessage},
numeric::{NumericUpDownBuilder, NumericUpDownMessage},
popup::{Placement, PopupBuilder, PopupMessage},
scroll_viewer::ScrollViewerBuilder,
stack_panel::StackPanelBuilder,
text::TextBuilder,
vec::{
vec2::{Vec2EditorBuilder, Vec2EditorMessage},
vec3::{Vec3EditorBuilder, Vec3EditorMessage},
vec4::{Vec4EditorBuilder, Vec4EditorMessage},
},
widget::{WidgetBuilder, WidgetMessage},
window::{WindowBuilder, WindowTitle},
BuildContext, Thickness, UiNode, UserInterface, VerticalAlignment,
},
material::{shader::Shader, Material, PropertyValue, SharedMaterial},
resource::texture::TextureState,
scene::{
base::BaseBuilder,
mesh::{
surface::{SurfaceBuilder, SurfaceData, SurfaceSharedData},
MeshBuilder,
},
},
utils::into_gui_texture,
};
use std::sync::mpsc::Sender;
struct TextureContextMenu {
popup: Handle<UiNode>,
show_in_asset_browser: Handle<UiNode>,
target: Handle<UiNode>,
}
impl TextureContextMenu {
fn new(ctx: &mut BuildContext) -> Self {
let show_in_asset_browser;
let popup = PopupBuilder::new(WidgetBuilder::new().with_visibility(false))
.with_content(
StackPanelBuilder::new(WidgetBuilder::new().with_child({
show_in_asset_browser = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Show In Asset Browser"))
.build(ctx);
show_in_asset_browser
}))
.build(ctx),
)
.build(ctx);
Self {
popup,
show_in_asset_browser,
target: Default::default(),
}
}
}
pub struct MaterialEditor {
pub window: Handle<UiNode>,
properties_panel: Handle<UiNode>,
properties: BiDirHashMap<ImmutableString, Handle<UiNode>>,
preview: PreviewPanel,
material: Option<SharedMaterial>,
available_shaders: Handle<UiNode>,
shaders_list: Vec<Shader>,
texture_context_menu: TextureContextMenu,
}
fn create_item_container(
ctx: &mut BuildContext,
name: &str,
item: Handle<UiNode>,
) -> Handle<UiNode> {
ctx[item].set_column(1);
GridBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(1.0))
.with_child(
TextBuilder::new(WidgetBuilder::new())
.with_text(name)
.with_vertical_text_alignment(VerticalAlignment::Center)
.build(ctx),
)
.with_child(item),
)
.add_row(Row::strict(24.0))
.add_column(Column::strict(150.0))
.add_column(Column::stretch())
.build(ctx)
}
fn create_array_view<T, B>(
ctx: &mut BuildContext,
value: &[T],
mut item_builder: B,
) -> Handle<UiNode>
where
T: Clone,
B: FnMut(&mut BuildContext, T) -> Handle<UiNode>,
{
ListViewBuilder::new(WidgetBuilder::new().with_height(24.0))
.with_items(value.iter().map(|v| item_builder(ctx, v.clone())).collect())
.build(ctx)
}
fn create_array_of_array_view<'a, T, B, I>(
ctx: &mut BuildContext,
value: I,
mut item_builder: B,
) -> Handle<UiNode>
where
T: 'a + Clone,
B: FnMut(&mut BuildContext, T) -> Handle<UiNode>,
I: Iterator<Item = &'a [T]>,
{
ListViewBuilder::new(WidgetBuilder::new().with_height(24.0))
.with_items(
value
.map(|v| create_array_view(ctx, v, &mut item_builder))
.collect(),
)
.build(ctx)
}
fn create_float_view(ctx: &mut BuildContext, value: f32) -> Handle<UiNode> {
NumericUpDownBuilder::new(WidgetBuilder::new().with_height(24.0))
.with_value(value)
.build(ctx)
}
fn create_int_view(ctx: &mut BuildContext, value: i32) -> Handle<UiNode> {
NumericUpDownBuilder::new(WidgetBuilder::new().with_height(24.0))
.with_value(value as f32)
.with_precision(0)
.with_max_value(i32::MAX as f32)
.with_min_value(-i32::MAX as f32)
.build(ctx)
}
fn create_uint_view(ctx: &mut BuildContext, value: u32) -> Handle<UiNode> {
NumericUpDownBuilder::new(WidgetBuilder::new().with_height(24.0))
.with_value(value as f32)
.with_precision(0)
.with_max_value(u32::MAX as f32)
.with_min_value(0.0)
.build(ctx)
}
fn create_vec2_view(ctx: &mut BuildContext, value: Vector2<f32>) -> Handle<UiNode> {
Vec2EditorBuilder::new(WidgetBuilder::new().with_height(24.0))
.with_value(value)
.build(ctx)
}
fn create_vec3_view(ctx: &mut BuildContext, value: Vector3<f32>) -> Handle<UiNode> {
Vec3EditorBuilder::new(WidgetBuilder::new().with_height(24.0))
.with_value(value)
.build(ctx)
}
fn create_vec4_view(ctx: &mut BuildContext, value: Vector4<f32>) -> Handle<UiNode> {
Vec4EditorBuilder::new(WidgetBuilder::new().with_height(24.0))
.with_value(value)
.build(ctx)
}
fn sync_array<T, B>(
ui: &mut UserInterface,
handle: Handle<UiNode>,
array: &[T],
mut item_builder: B,
) where
T: Clone,
B: FnMut(&mut BuildContext, T) -> Handle<UiNode>,
{
let ctx = &mut ui.build_ctx();
let new_items = array.iter().map(|v| item_builder(ctx, v.clone())).collect();
send_sync_message(
ui,
ListViewMessage::items(handle, MessageDirection::ToWidget, new_items),
);
}
fn sync_array_of_arrays<'a, T, I, B>(
ui: &mut UserInterface,
handle: Handle<UiNode>,
array: I,
mut item_builder: B,
) where
T: 'a + Clone,
I: Iterator<Item = &'a [T]>,
B: FnMut(&mut BuildContext, T) -> Handle<UiNode>,
{
let ctx = &mut ui.build_ctx();
let new_items = array
.map(|v| create_array_view(ctx, v, &mut item_builder))
.collect();
send_sync_message(
ui,
ListViewMessage::items(handle, MessageDirection::ToWidget, new_items),
);
}
impl MaterialEditor {
pub fn new(engine: &mut GameEngine) -> Self {
let mut preview = PreviewPanel::new(engine, 300, 400);
let graph = &mut engine.scenes[preview.scene()].graph;
let sphere = MeshBuilder::new(BaseBuilder::new())
.with_surfaces(vec![SurfaceBuilder::new(SurfaceSharedData::new(
SurfaceData::make_sphere(30, 30, 1.0, &Matrix4::identity()),
))
.build()])
.build(graph);
preview.set_model(sphere, engine);
let ctx = &mut engine.user_interface.build_ctx();
let panel;
let properties_panel;
let available_shaders;
let window = WindowBuilder::new(WidgetBuilder::new().with_width(300.0))
.open(false)
.with_title(WindowTitle::text("Material Editor"))
.with_content(
GridBuilder::new(
WidgetBuilder::new()
.with_child(
GridBuilder::new(
WidgetBuilder::new()
.on_row(0)
.with_child(
TextBuilder::new(
WidgetBuilder::new().on_row(0).on_column(0),
)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_text("Shader")
.build(ctx),
)
.with_child({
available_shaders = DropdownListBuilder::new(
WidgetBuilder::new().on_column(1),
)
.with_close_on_selection(true)
.build(ctx);
available_shaders
}),
)
.add_column(Column::strict(150.0))
.add_column(Column::stretch())
.add_row(Row::strict(25.0))
.build(ctx),
)
.with_child(
ScrollViewerBuilder::new(WidgetBuilder::new().on_row(1))
.with_content({
properties_panel =
StackPanelBuilder::new(WidgetBuilder::new()).build(ctx);
properties_panel
})
.build(ctx),
)
.with_child({
panel = BorderBuilder::new(WidgetBuilder::new().on_row(2).on_column(0))
.build(ctx);
panel
}),
)
.add_row(Row::strict(26.0))
.add_row(Row::stretch())
.add_row(Row::strict(300.0))
.add_column(Column::stretch())
.build(ctx),
)
.build(ctx);
ctx.link(preview.root, panel);
let mut editor = Self {
texture_context_menu: TextureContextMenu::new(ctx),
window,
preview,
properties_panel,
properties: Default::default(),
material: None,
available_shaders,
shaders_list: Default::default(),
};
editor.sync_available_shaders_list(engine.resource_manager.clone());
editor
}
pub fn sync_available_shaders_list(&mut self, resource_manager: ResourceManager) {
self.shaders_list.clear();
self.shaders_list
.extend_from_slice(&Shader::standard_shaders());
for dir in fyrox::walkdir::WalkDir::new(".").into_iter().flatten() {
let path = dir.path();
if let Some(extension) = path.extension() {
if extension == "shader" {
if let Ok(relative_path) = make_relative_path(path) {
self.shaders_list
.push(resource_manager.request_shader(relative_path));
}
}
}
}
block_on(fyrox::core::futures::future::join_all(
self.shaders_list.iter().cloned(),
));
}
pub fn create_shaders_items(&self, ui: &mut UserInterface, material: &Material) {
let items = self
.shaders_list
.iter()
.map(|s| make_dropdown_list_option(&mut ui.build_ctx(), &s.data_ref().definition.name))
.collect::<Vec<_>>();
send_sync_message(
ui,
DropdownListMessage::items(self.available_shaders, MessageDirection::ToWidget, items),
);
send_sync_message(
ui,
DropdownListMessage::selection(
self.available_shaders,
MessageDirection::ToWidget,
self.shaders_list
.iter()
.position(|s| material.shader().key() == s.key()),
),
)
}
pub fn set_material(&mut self, material: Option<SharedMaterial>, engine: &mut GameEngine) {
self.material = material;
if let Some(material) = self.material.clone() {
engine.scenes[self.preview.scene()].graph[self.preview.model()]
.as_mesh_mut()
.surfaces_mut()
.first_mut()
.unwrap()
.set_material(material);
}
self.sync_to_model(&mut engine.user_interface);
}
pub fn sync_to_model(&mut self, ui: &mut UserInterface) {
if let Some(material) = self.material.as_ref() {
let material = material.lock();
for name in self
.properties
.forward_map()
.keys()
.cloned()
.collect::<Vec<_>>()
{
if !material.properties().contains_key(&name) {
let item_to_delete = ui
.node(
self.properties
.remove_by_key(&name)
.expect("Desync has occurred!"),
)
.parent();
send_sync_message(
ui,
WidgetMessage::remove(item_to_delete, MessageDirection::ToWidget),
);
}
}
let mut sorted_properties = material
.properties()
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<Vec<_>>();
sorted_properties.sort_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
for (name, property_value) in sorted_properties.iter() {
if !self.properties.contains_key(name) {
let ctx = &mut ui.build_ctx();
let item = match property_value {
PropertyValue::Float(value) => create_float_view(ctx, *value),
PropertyValue::FloatArray(value) => {
create_array_view(ctx, value, create_float_view)
}
PropertyValue::Int(value) => create_int_view(ctx, *value),
PropertyValue::IntArray(value) => {
create_array_view(ctx, value, create_int_view)
}
PropertyValue::UInt(value) => create_uint_view(ctx, *value),
PropertyValue::UIntArray(value) => {
create_array_view(ctx, value, create_uint_view)
}
PropertyValue::Vector2(value) => create_vec2_view(ctx, *value),
PropertyValue::Vector2Array(value) => {
create_array_view(ctx, value, create_vec2_view)
}
PropertyValue::Vector3(value) => create_vec3_view(ctx, *value),
PropertyValue::Vector3Array(value) => {
create_array_view(ctx, value, create_vec3_view)
}
PropertyValue::Vector4(value) => create_vec4_view(ctx, *value),
PropertyValue::Vector4Array(value) => {
create_array_view(ctx, value, create_vec4_view)
}
PropertyValue::Matrix2(value) => {
create_array_view(ctx, value.data.as_slice(), create_float_view)
}
PropertyValue::Matrix2Array(value) => create_array_of_array_view(
ctx,
value.iter().map(|m| m.data.as_slice()),
create_float_view,
),
PropertyValue::Matrix3(value) => {
create_array_view(ctx, value.data.as_slice(), create_float_view)
}
PropertyValue::Matrix3Array(value) => create_array_of_array_view(
ctx,
value.iter().map(|m| m.data.as_slice()),
create_float_view,
),
PropertyValue::Matrix4(value) => {
create_array_view(ctx, value.data.as_slice(), create_float_view)
}
PropertyValue::Matrix4Array(value) => create_array_of_array_view(
ctx,
value.iter().map(|m| m.data.as_slice()),
create_float_view,
),
PropertyValue::Bool(value) => CheckBoxBuilder::new(WidgetBuilder::new())
.checked(Some(*value))
.build(ctx),
PropertyValue::Color(value) => ColorFieldBuilder::new(WidgetBuilder::new())
.with_color(*value)
.build(ctx),
PropertyValue::Sampler { value, .. } => ImageBuilder::new(
WidgetBuilder::new()
.with_allow_drop(true)
.with_context_menu(self.texture_context_menu.popup),
)
.with_opt_texture(value.clone().map(into_gui_texture))
.build(ctx),
};
self.properties.insert(name.to_owned(), item);
let container = create_item_container(ctx, name, item);
send_sync_message(
ui,
WidgetMessage::link(
container,
MessageDirection::ToWidget,
self.properties_panel,
),
);
}
}
for (name, property_value) in material.properties() {
let item = *self
.properties
.value_of(name)
.unwrap_or_else(|| panic!("Property not found {}", name));
match property_value {
PropertyValue::Float(value) => {
send_sync_message(
ui,
NumericUpDownMessage::value(item, MessageDirection::ToWidget, *value),
);
}
PropertyValue::FloatArray(value) => {
sync_array(ui, item, value, create_float_view)
}
PropertyValue::Int(value) => {
send_sync_message(
ui,
NumericUpDownMessage::value(
item,
MessageDirection::ToWidget,
*value as f32,
),
);
}
PropertyValue::IntArray(value) => sync_array(ui, item, value, create_int_view),
PropertyValue::UInt(value) => {
send_sync_message(
ui,
NumericUpDownMessage::value(
item,
MessageDirection::ToWidget,
*value as f32,
),
);
}
PropertyValue::UIntArray(value) => {
sync_array(ui, item, value, create_uint_view)
}
PropertyValue::Vector2(value) => send_sync_message(
ui,
Vec2EditorMessage::value(item, MessageDirection::ToWidget, *value),
),
PropertyValue::Vector2Array(value) => {
sync_array(ui, item, value, create_vec2_view)
}
PropertyValue::Vector3(value) => send_sync_message(
ui,
Vec3EditorMessage::value(item, MessageDirection::ToWidget, *value),
),
PropertyValue::Vector3Array(value) => {
sync_array(ui, item, value, create_vec3_view)
}
PropertyValue::Vector4(value) => send_sync_message(
ui,
Vec4EditorMessage::value(item, MessageDirection::ToWidget, *value),
),
PropertyValue::Vector4Array(value) => {
sync_array(ui, item, value, create_vec4_view)
}
PropertyValue::Matrix2(value) => {
sync_array(ui, item, value.as_slice(), create_float_view)
}
PropertyValue::Matrix2Array(value) => sync_array_of_arrays(
ui,
item,
value.iter().map(|m| m.as_slice()),
create_float_view,
),
PropertyValue::Matrix3(value) => {
sync_array(ui, item, value.as_slice(), create_float_view)
}
PropertyValue::Matrix3Array(value) => sync_array_of_arrays(
ui,
item,
value.iter().map(|m| m.as_slice()),
create_float_view,
),
PropertyValue::Matrix4(value) => {
sync_array(ui, item, value.as_slice(), create_float_view)
}
PropertyValue::Matrix4Array(value) => sync_array_of_arrays(
ui,
item,
value.iter().map(|m| m.as_slice()),
create_float_view,
),
PropertyValue::Bool(value) => {
send_sync_message(
ui,
CheckBoxMessage::checked(
item,
MessageDirection::ToWidget,
Some(*value),
),
);
}
PropertyValue::Color(value) => {
send_sync_message(
ui,
ColorFieldMessage::color(item, MessageDirection::ToWidget, *value),
);
}
PropertyValue::Sampler { value, .. } => send_sync_message(
ui,
ImageMessage::texture(
item,
MessageDirection::ToWidget,
value.clone().map(into_gui_texture),
),
),
}
}
self.create_shaders_items(ui, &material);
} else {
send_sync_message(
ui,
ListViewMessage::items(self.properties_panel, MessageDirection::ToWidget, vec![]),
);
}
}
pub fn handle_ui_message(
&mut self,
message: &UiMessage,
engine: &mut GameEngine,
sender: &Sender<Message>,
) {
self.preview.handle_message(message, engine);
if let Some(material) = self.material.clone() {
if let Some(msg) = message.data::<DropdownListMessage>() {
if message.destination() == self.available_shaders
&& message.direction() == MessageDirection::FromWidget
{
match msg {
DropdownListMessage::SelectionChanged(Some(value)) => {
sender
.send(Message::do_scene_command(SetMaterialShaderCommand::new(
material.clone(),
self.shaders_list[*value].clone(),
)))
.unwrap();
}
DropdownListMessage::Open => {
self.sync_available_shaders_list(engine.resource_manager.clone());
self.create_shaders_items(&mut engine.user_interface, &material.lock());
}
_ => (),
}
}
} else if let Some(PopupMessage::Placement(Placement::Cursor(target))) =
message.data::<PopupMessage>()
{
if message.destination() == self.texture_context_menu.popup {
self.texture_context_menu.target = *target;
}
} else if let Some(MenuItemMessage::Click) = message.data::<MenuItemMessage>() {
if message.destination() == self.texture_context_menu.show_in_asset_browser
&& self.texture_context_menu.target.is_some()
{
let path = engine
.user_interface
.node(self.texture_context_menu.target)
.cast::<Image>()
.unwrap()
.texture
.clone()
.and_then(|t| {
t.0.downcast::<Mutex<TextureState>>()
.map(|t| t.lock().path().to_path_buf())
.ok()
});
if let Some(path) = path {
sender.send(Message::ShowInAssetBrowser(path)).unwrap();
}
}
}
if let Some(property_name) = self.properties.key_of(&message.destination()) {
let property_value = if let Some(NumericUpDownMessage::Value(value)) =
message.data::<NumericUpDownMessage<f32>>()
{
if message.direction() == MessageDirection::FromWidget {
match material.lock().property_ref(property_name).unwrap() {
PropertyValue::Float(_) => Some(PropertyValue::Float(*value)),
PropertyValue::Int(_) => Some(PropertyValue::Int(*value as i32)),
PropertyValue::UInt(_) => Some(PropertyValue::UInt(*value as u32)),
_ => None,
}
} else {
None
}
} else if let Some(Vec2EditorMessage::Value(value)) =
message.data::<Vec2EditorMessage<f32>>()
{
if message.direction() == MessageDirection::FromWidget {
Some(PropertyValue::Vector2(*value))
} else {
None
}
} else if let Some(Vec3EditorMessage::Value(value)) =
message.data::<Vec3EditorMessage<f32>>()
{
if message.direction() == MessageDirection::FromWidget {
Some(PropertyValue::Vector3(*value))
} else {
None
}
} else if let Some(Vec4EditorMessage::Value(value)) =
message.data::<Vec4EditorMessage<f32>>()
{
if message.direction() == MessageDirection::FromWidget {
Some(PropertyValue::Vector4(*value))
} else {
None
}
} else if let Some(ColorFieldMessage::Color(color)) =
message.data::<ColorFieldMessage>()
{
if message.direction() == MessageDirection::FromWidget {
Some(PropertyValue::Color(*color))
} else {
None
}
} else if let Some(WidgetMessage::Drop(handle)) = message.data::<WidgetMessage>() {
if let Some(asset_item) =
engine.user_interface.node(*handle).cast::<AssetItem>()
{
if let Ok(relative_path) = make_relative_path(&asset_item.path) {
let texture =
Some(engine.resource_manager.request_texture(relative_path));
engine.user_interface.send_message(ImageMessage::texture(
message.destination(),
MessageDirection::ToWidget,
texture.clone().map(into_gui_texture),
));
Some(PropertyValue::Sampler {
value: texture,
fallback: Default::default(),
})
} else {
None
}
} else {
None
}
} else {
None
};
if let Some(property_value) = property_value {
sender
.send(Message::do_scene_command(
SetMaterialPropertyValueCommand::new(
material,
property_name.clone(),
property_value,
),
))
.unwrap();
}
}
}
}
pub fn update(&mut self, engine: &mut GameEngine) {
self.preview.update(engine)
}
}