Skip to main content

fyroxed_base/plugins/material/
editor.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21use crate::{
22    asset::{
23        item::{AssetItem, AssetItemMessage},
24        preview::cache::IconRequest,
25        selector::AssetSelectorMixin,
26    },
27    fyrox::{
28        asset::{core::pool::Handle, manager::ResourceManager, state::ResourceState},
29        core::{
30            color::Color, log::Log, parking_lot::Mutex, reflect::prelude::*,
31            type_traits::prelude::*, uuid_provider, visitor::prelude::*, SafeLock,
32        },
33        graph::SceneGraph,
34        gui::{
35            brush::Brush,
36            button::{Button, ButtonBuilder, ButtonMessage},
37            draw::{CommandTexture, Draw, DrawingContext},
38            grid::{Column, GridBuilder, Row},
39            image::{Image, ImageBuilder, ImageMessage},
40            inspector::{
41                editors::{
42                    PropertyEditorBuildContext, PropertyEditorDefinition, PropertyEditorInstance,
43                    PropertyEditorMessageContext, PropertyEditorTranslationContext,
44                },
45                FieldAction, InspectorError, PropertyChanged,
46            },
47            message::{MessageData, UiMessage},
48            text::{Text, TextBuilder, TextMessage},
49            utils::{make_asset_preview_tooltip, make_simple_tooltip, ImageButtonBuilder},
50            widget::{Widget, WidgetBuilder, WidgetMessage},
51            BuildContext, Control, Thickness, UiNode, UserInterface, VerticalAlignment,
52        },
53        material::{Material, MaterialResource, MaterialResourceExtension},
54    },
55    load_image,
56    message::MessageSender,
57    plugins::inspector::EditorEnvironment,
58    utils::make_pick_button,
59    Message, MessageDirection,
60};
61use std::{
62    any::TypeId,
63    fmt::{Debug, Formatter},
64    ops::{Deref, DerefMut},
65    sync::mpsc::Sender,
66};
67
68#[derive(Debug, Clone, PartialEq)]
69pub enum MaterialFieldMessage {
70    Material(MaterialResource),
71}
72impl MessageData for MaterialFieldMessage {}
73
74#[derive(Clone, Visit, Reflect, ComponentProvider)]
75#[reflect(derived_type = "UiNode")]
76pub struct MaterialFieldEditor {
77    widget: Widget,
78    #[visit(skip)]
79    #[reflect(hidden)]
80    sender: MessageSender,
81    text: Handle<Text>,
82    edit: Handle<Button>,
83    locate: Handle<Button>,
84    make_unique: Handle<Button>,
85    material: MaterialResource,
86    image: Handle<Image>,
87    image_preview: Handle<Image>,
88    asset_selector_mixin: AssetSelectorMixin<Material>,
89}
90
91impl Debug for MaterialFieldEditor {
92    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
93        write!(f, "MaterialFieldEditor")
94    }
95}
96
97impl Deref for MaterialFieldEditor {
98    type Target = Widget;
99
100    fn deref(&self) -> &Self::Target {
101        &self.widget
102    }
103}
104
105impl DerefMut for MaterialFieldEditor {
106    fn deref_mut(&mut self) -> &mut Self::Target {
107        &mut self.widget
108    }
109}
110
111uuid_provider!(MaterialFieldEditor = "d3fa0a7c-52d6-4cca-885e-0db8b18542e2");
112
113impl Control for MaterialFieldEditor {
114    fn draw(&self, drawing_context: &mut DrawingContext) {
115        // Emit transparent geometry for the field to be able to catch mouse events without precise
116        // pointing.
117        drawing_context.push_rect_filled(&self.bounding_rect(), None);
118        drawing_context.commit(
119            self.clip_bounds(),
120            Brush::Solid(Color::TRANSPARENT),
121            CommandTexture::None,
122            &self.widget.material,
123            None,
124        );
125    }
126
127    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
128        self.widget.handle_routed_message(ui, message);
129
130        if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
131            if message.destination() == self.edit {
132                self.sender
133                    .send(Message::OpenMaterialEditor(self.material.clone()));
134            } else if message.destination() == self.make_unique {
135                ui.send(
136                    self.handle,
137                    MaterialFieldMessage::Material(self.material.deep_copy_as_embedded()),
138                );
139            } else if message.destination() == self.locate {
140                if let Some(path) = self
141                    .asset_selector_mixin
142                    .resource_manager
143                    .resource_path(&self.material)
144                {
145                    self.sender.send(Message::ShowInAssetBrowser(path));
146                }
147            }
148        } else if let Some(MaterialFieldMessage::Material(material)) = message.data_for(self.handle)
149        {
150            if &self.material != material {
151                self.material = material.clone();
152
153                ui.send(
154                    self.text,
155                    TextMessage::Text(make_name(
156                        &self.asset_selector_mixin.resource_manager,
157                        &self.material,
158                    )),
159                );
160
161                self.asset_selector_mixin
162                    .request_preview(self.handle, material);
163
164                ui.try_send_response(message);
165            }
166        } else if let Some(WidgetMessage::Drop(dropped)) = message.data() {
167            if let Some(item) = ui.node(*dropped).cast::<AssetItem>() {
168                if let Some(material) = item.resource::<Material>() {
169                    ui.send(self.handle(), MaterialFieldMessage::Material(material));
170                }
171            }
172        } else if let Some(AssetItemMessage::Icon {
173            texture,
174            flip_y,
175            color,
176        }) = message.data_for(self.handle)
177        {
178            for widget in [self.image, self.image_preview] {
179                ui.send(widget, ImageMessage::Texture(texture.clone()));
180                ui.send(widget, ImageMessage::Flip(*flip_y));
181                ui.send(
182                    widget,
183                    WidgetMessage::Background(Brush::Solid(*color).into()),
184                )
185            }
186        }
187
188        self.asset_selector_mixin
189            .handle_ui_message(Some(&self.material), ui, message);
190    }
191
192    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
193        self.asset_selector_mixin
194            .preview_ui_message(ui, message, |resource| {
195                UiMessage::for_widget(
196                    self.handle,
197                    MaterialFieldMessage::Material(resource.try_cast::<Material>().unwrap()),
198                )
199            });
200    }
201}
202
203pub struct MaterialFieldEditorBuilder {
204    widget_builder: WidgetBuilder,
205}
206
207fn make_name(resource_manager: &ResourceManager, material: &MaterialResource) -> String {
208    let resource_path = resource_manager.state().resource_path(material.as_ref());
209    let header = material.header();
210    match header.state {
211        ResourceState::Ok { .. } => {
212            if let Some(path) = resource_path {
213                format!(
214                    "{} - {} uses; id - {}",
215                    path.display(),
216                    material.use_count(),
217                    material.key()
218                )
219            } else {
220                format!(
221                    "Embedded - {} uses; id - {}",
222                    material.use_count(),
223                    material.key()
224                )
225            }
226        }
227        ResourceState::Unloaded => "Material not loading".into(),
228        ResourceState::LoadError { ref error, .. } => {
229            format!("Loading failed: {error:?}")
230        }
231        ResourceState::Pending { .. } => {
232            format!("Loading {}", header.kind)
233        }
234    }
235}
236
237impl MaterialFieldEditorBuilder {
238    pub fn new(widget_builder: WidgetBuilder) -> Self {
239        Self { widget_builder }
240    }
241
242    pub fn build(
243        self,
244        ctx: &mut BuildContext,
245        sender: MessageSender,
246        material: MaterialResource,
247        icon_request_sender: Sender<IconRequest>,
248        resource_manager: ResourceManager,
249    ) -> Handle<MaterialFieldEditor> {
250        let edit;
251        let text;
252        let select;
253        let locate;
254        let make_unique;
255        let make_unique_tooltip = "Creates a deep copy of the material, making a separate version of the material. \
256        Useful when you need to change some properties in the material, but only on some nodes that uses the material.";
257
258        let buttons = GridBuilder::new(
259            WidgetBuilder::new()
260                .on_row(1)
261                .with_child({
262                    select = make_pick_button(0, ctx);
263                    select
264                })
265                .with_child({
266                    locate = ImageButtonBuilder::default()
267                        .on_column(1)
268                        .with_image(load_image!("../../../resources/locate.png"))
269                        .with_tooltip("Show In Asset Browser")
270                        .build_button(ctx);
271                    locate
272                })
273                .with_child({
274                    edit = ButtonBuilder::new(
275                        WidgetBuilder::new()
276                            .with_width(55.0)
277                            .with_margin(Thickness::uniform(1.0))
278                            .on_column(2),
279                    )
280                    .with_text("Edit...")
281                    .build(ctx);
282                    edit
283                })
284                .with_child({
285                    make_unique = ButtonBuilder::new(
286                        WidgetBuilder::new()
287                            .with_width(100.0)
288                            .with_margin(Thickness::uniform(1.0))
289                            .on_column(3)
290                            .with_tooltip(make_simple_tooltip(ctx, make_unique_tooltip)),
291                    )
292                    .with_text("Make Unique")
293                    .build(ctx);
294                    make_unique
295                }),
296        )
297        .add_row(Row::strict(24.0))
298        .add_column(Column::auto())
299        .add_column(Column::auto())
300        .add_column(Column::auto())
301        .add_column(Column::auto())
302        .build(ctx);
303
304        let (image_preview_tooltip, image_preview) = make_asset_preview_tooltip(None, ctx);
305
306        let image = ImageBuilder::new(
307            WidgetBuilder::new()
308                .on_column(0)
309                .with_width(52.0)
310                .with_height(52.0)
311                .with_tooltip(image_preview_tooltip)
312                .with_margin(Thickness::uniform(1.0)),
313        )
314        .build(ctx);
315
316        let content = GridBuilder::new(
317            WidgetBuilder::new()
318                .on_column(1)
319                .with_child({
320                    text =
321                        TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::uniform(1.0)))
322                            .with_text(make_name(&resource_manager, &material))
323                            .with_vertical_text_alignment(VerticalAlignment::Center)
324                            .build(ctx);
325                    text
326                })
327                .with_child(buttons),
328        )
329        .add_row(Row::auto())
330        .add_row(Row::auto())
331        .add_column(Column::stretch())
332        .build(ctx);
333
334        let editor = MaterialFieldEditor {
335            widget: self
336                .widget_builder
337                .with_preview_messages(true)
338                .with_allow_drop(true)
339                .with_child(
340                    GridBuilder::new(WidgetBuilder::new().with_child(image).with_child(content))
341                        .add_column(Column::auto())
342                        .add_column(Column::stretch())
343                        .add_row(Row::auto())
344                        .build(ctx),
345                )
346                .build(ctx),
347            edit,
348            sender,
349            material: material.clone(),
350            text,
351            make_unique,
352            asset_selector_mixin: AssetSelectorMixin::new(
353                select,
354                icon_request_sender.clone(),
355                resource_manager,
356            ),
357            image,
358            image_preview,
359            locate,
360        };
361
362        let handle = ctx.add(editor);
363
364        Log::verify(icon_request_sender.send(IconRequest {
365            widget_handle: handle.to_base(),
366            resource: material.into_untyped(),
367            force_update: false,
368        }));
369
370        handle
371    }
372}
373
374#[derive(Debug)]
375pub struct MaterialPropertyEditorDefinition {
376    pub sender: Mutex<MessageSender>,
377}
378
379impl PropertyEditorDefinition for MaterialPropertyEditorDefinition {
380    fn value_type_id(&self) -> TypeId {
381        TypeId::of::<MaterialResource>()
382    }
383
384    fn create_instance(
385        &self,
386        ctx: PropertyEditorBuildContext,
387    ) -> Result<PropertyEditorInstance, InspectorError> {
388        let value = ctx.property_info.cast_value::<MaterialResource>()?;
389        let environment = EditorEnvironment::try_get_from(&ctx.environment)?;
390        Ok(PropertyEditorInstance::simple(
391            MaterialFieldEditorBuilder::new(WidgetBuilder::new()).build(
392                ctx.build_context,
393                self.sender.safe_lock().clone(),
394                value.clone(),
395                environment.icon_request_sender.clone(),
396                environment.resource_manager.clone(),
397            ),
398        ))
399    }
400
401    fn create_message(
402        &self,
403        ctx: PropertyEditorMessageContext,
404    ) -> Result<Option<UiMessage>, InspectorError> {
405        let value = ctx.property_info.cast_value::<MaterialResource>()?;
406        Ok(Some(UiMessage::for_widget(
407            ctx.instance,
408            MaterialFieldMessage::Material(value.clone()),
409        )))
410    }
411
412    fn translate_message(&self, ctx: PropertyEditorTranslationContext) -> Option<PropertyChanged> {
413        if ctx.message.direction() == MessageDirection::FromWidget {
414            if let Some(MaterialFieldMessage::Material(value)) = ctx.message.data() {
415                return Some(PropertyChanged {
416                    name: ctx.name.to_string(),
417                    action: FieldAction::object(value.clone()),
418                });
419            }
420        }
421        None
422    }
423}
424
425#[cfg(test)]
426mod test {
427    use crate::plugins::material::editor::MaterialFieldEditorBuilder;
428    use fyrox::asset::io::FsResourceIo;
429    use fyrox::asset::manager::ResourceManager;
430    use fyrox::core::task::TaskPool;
431    use fyrox::{gui::test::test_widget_deletion, gui::widget::WidgetBuilder};
432    use std::sync::mpsc::channel;
433    use std::sync::Arc;
434
435    #[test]
436    fn test_deletion() {
437        let resource_manager =
438            ResourceManager::new(Arc::new(FsResourceIo), Arc::new(TaskPool::new()));
439        let (sender, _) = channel();
440        test_widget_deletion(|ctx| {
441            MaterialFieldEditorBuilder::new(WidgetBuilder::new()).build(
442                ctx,
443                Default::default(),
444                Default::default(),
445                sender,
446                resource_manager,
447            )
448        });
449    }
450}