Files
fyroxengine--fyrox/editor/src/plugins/curve_editor.rs
T
2026-07-13 12:24:51 +08:00

587 lines
23 KiB
Rust

// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::{
command::{Command, CommandContext, CommandStack, CommandTrait},
fyrox::{
asset::manager::ResourceManager,
asset::Resource,
core::{
futures::executor::block_on, math::curve::Curve, pool::Handle, reflect::prelude::*,
some_or_return, visitor::prelude::*,
},
engine::Engine,
gui::{
border::BorderBuilder,
button::{ButtonBuilder, ButtonMessage},
curve::{CurveEditorBuilder, CurveEditorMessage},
file_browser::FileSelectorMessage,
grid::{Column, GridBuilder, Row},
menu::{MenuBuilder, MenuItemBuilder, MenuItemContent, MenuItemMessage},
message::UiMessage,
messagebox::{MessageBoxBuilder, MessageBoxResult},
stack_panel::StackPanelBuilder,
widget::{WidgetBuilder, WidgetMessage},
window::{WindowBuilder, WindowMessage, WindowTitle},
BuildContext, HorizontalAlignment, Orientation, Thickness, UserInterface,
},
gui::{
button::Button,
curve::CurveEditor,
file_browser::{FileSelector, FileSelectorMode, FileType},
menu::MenuItem,
messagebox::MessageBox,
style::resource::StyleResourceExt,
style::Style,
window::{Window, WindowAlignment},
},
resource::curve::{CurveResource, CurveResourceState},
},
menu::create_menu_item,
plugin::EditorPlugin,
utils::create_file_selector,
Editor, MessageBoxButtons, MessageBoxMessage,
};
use std::{fmt::Debug, path::PathBuf};
#[derive(PartialEq, Reflect, Clone, Debug)]
#[reflect(type_uuid = "f29aa9c7-e0c4-4602-8588-3e8a20ab7cdc")]
pub struct CurveEditorContext {}
impl CommandContext for CurveEditorContext {}
#[derive(Debug)]
struct ModifyCurveCommand {
curve_resource: CurveResource,
curve: Curve,
}
impl ModifyCurveCommand {
fn swap(&mut self) {
std::mem::swap(&mut self.curve_resource.data_ref().curve, &mut self.curve);
}
}
impl CommandTrait for ModifyCurveCommand {
fn name(&mut self, _: &dyn CommandContext) -> String {
"Modify Curve".to_owned()
}
fn execute(&mut self, _: &mut dyn CommandContext) {
self.swap();
}
fn revert(&mut self, _: &mut dyn CommandContext) {
self.swap();
}
}
struct FileMenu {
new: Handle<MenuItem>,
save: Handle<MenuItem>,
load: Handle<MenuItem>,
}
struct EditMenu {
undo: Handle<MenuItem>,
redo: Handle<MenuItem>,
}
struct Menu {
file: FileMenu,
edit: EditMenu,
}
pub struct CurveEditorWindow {
window: Handle<Window>,
curve_editor: Handle<CurveEditor>,
ok: Handle<Button>,
cancel: Handle<Button>,
curve_resource: Option<CurveResource>,
command_stack: CommandStack,
menu: Menu,
load_file_selector: Handle<FileSelector>,
save_file_selector: Handle<FileSelector>,
path: PathBuf,
save_changes_message_box: Handle<MessageBox>,
cancel_message_box: Handle<MessageBox>,
modified: bool,
backup: Curve,
}
impl CurveEditorWindow {
pub fn new(ctx: &mut BuildContext) -> Self {
let file_type = FileType::new()
.with_extension("crv")
.with_description("Curve");
let load_file_selector =
create_file_selector(ctx, file_type.clone(), FileSelectorMode::Open);
let save_file_selector = create_file_selector(
ctx,
file_type,
FileSelectorMode::Save {
default_file_name: PathBuf::from("unnamed.crv"),
},
);
let save_changes_message_box = MessageBoxBuilder::new(
WindowBuilder::new(WidgetBuilder::new())
.open(false)
.with_title(WindowTitle::text("Unsaved Changes")),
)
.with_text(
"You have unsaved changes, do you want to save it before closing the curve editor?",
)
.with_buttons(MessageBoxButtons::YesNoCancel)
.build(ctx);
let cancel_message_box = MessageBoxBuilder::new(
WindowBuilder::new(WidgetBuilder::new())
.open(false)
.with_title(WindowTitle::text("Unsaved Changes")),
)
.with_text("You have unsaved changes, do you want to quit the curve editor without saving?")
.with_buttons(MessageBoxButtons::YesNo)
.build(ctx);
let curve_editor;
let ok;
let cancel;
let new;
let save;
let load;
let undo;
let redo;
let window = WindowBuilder::new(WidgetBuilder::new().with_width(400.0).with_height(300.0))
.open(false)
.with_content(
GridBuilder::new(
WidgetBuilder::new()
.with_child(
MenuBuilder::new(WidgetBuilder::new())
.with_items(vec![
MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("File"))
.with_items(vec![
{
new = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(
MenuItemContent::text_with_shortcut(
"New", "Ctrl+N",
),
)
.build(ctx);
new
},
{
load = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(
MenuItemContent::text_with_shortcut(
"Load", "Ctrl+L",
),
)
.build(ctx);
load
},
{
save = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(
MenuItemContent::text_with_shortcut(
"Save", "Ctrl+S",
),
)
.build(ctx);
save
},
])
.build(ctx),
MenuItemBuilder::new(WidgetBuilder::new())
.with_content(MenuItemContent::text("Edit"))
.with_items(vec![
{
undo = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(
MenuItemContent::text_with_shortcut(
"Undo", "Ctrl+Z",
),
)
.build(ctx);
undo
},
{
redo = MenuItemBuilder::new(WidgetBuilder::new())
.with_content(
MenuItemContent::text_with_shortcut(
"Redo", "Ctrl+Y",
),
)
.build(ctx);
redo
},
])
.build(ctx),
])
.build(ctx),
)
.with_child(
BorderBuilder::new(
WidgetBuilder::new()
.on_row(1)
.on_column(0)
.with_background(ctx.style.property(Style::BRUSH_DARKEST))
.with_child({
curve_editor = CurveEditorBuilder::new(
WidgetBuilder::new().with_enabled(false),
)
.build(ctx);
curve_editor
}),
)
.build(ctx),
)
.with_child(
StackPanelBuilder::new(
WidgetBuilder::new()
.on_row(2)
.on_column(0)
.with_horizontal_alignment(HorizontalAlignment::Right)
.with_child({
ok = ButtonBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(1.0))
.with_width(100.0),
)
.with_text("OK")
.build(ctx);
ok
})
.with_child({
cancel = ButtonBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(1.0))
.with_width(100.0),
)
.with_text("Cancel")
.build(ctx);
cancel
}),
)
.with_orientation(Orientation::Horizontal)
.build(ctx),
),
)
.add_row(Row::strict(25.0))
.add_row(Row::stretch())
.add_row(Row::strict(25.0))
.add_column(Column::stretch())
.build(ctx),
)
.with_remove_on_close(true)
.with_title(WindowTitle::text("Curve Editor"))
.with_tab_label("Curve")
.build(ctx);
Self {
window,
curve_editor,
ok,
cancel,
curve_resource: None,
command_stack: CommandStack::new(false, 2048),
menu: Menu {
file: FileMenu { new, save, load },
edit: EditMenu { undo, redo },
},
load_file_selector,
save_file_selector,
path: Default::default(),
save_changes_message_box,
modified: false,
backup: Default::default(),
cancel_message_box,
}
}
fn destroy(self, ui: &UserInterface) {
ui.send(self.cancel_message_box, WidgetMessage::Remove);
ui.send(self.save_changes_message_box, WidgetMessage::Remove);
ui.send(self.load_file_selector, WidgetMessage::Remove);
ui.send(self.save_file_selector, WidgetMessage::Remove);
ui.send(self.window, WindowMessage::Close);
}
pub fn open(&self, ui: &UserInterface) {
ui.send(
self.window,
WindowMessage::Open {
alignment: WindowAlignment::Center,
modal: true,
focus_content: true,
},
);
}
fn sync_to_model(&mut self, ui: &UserInterface) {
if let Some(curve_resource) = self.curve_resource.as_ref() {
ui.send_sync(
self.curve_editor,
CurveEditorMessage::Sync(vec![curve_resource.data_ref().curve.clone()]),
);
}
}
fn save(&self) {
if let Some(curve_resource) = self.curve_resource.as_ref() {
if let Some(state) = curve_resource.state().data() {
let mut visitor = Visitor::new();
state.curve.visit("Curve", &mut visitor).unwrap();
visitor.save_binary_to_file(&self.path).unwrap();
}
}
}
fn set_curve(
&mut self,
resource_manager: &ResourceManager,
curve: CurveResource,
ui: &UserInterface,
) {
self.backup = curve.data_ref().curve.clone();
self.curve_resource = Some(curve);
ui.send(self.curve_editor, WidgetMessage::Enabled(true));
self.sync_to_model(ui);
self.sync_title(resource_manager, ui);
self.modified = false;
self.command_stack.clear(&mut CurveEditorContext {});
}
fn sync_title(&self, resource_manager: &ResourceManager, ui: &UserInterface) {
let title = if let Some(curve_resource) = self.curve_resource.as_ref() {
match resource_manager.resource_path(curve_resource.as_ref()) {
Some(path) => {
format!("Curve Editor - {}", path.display())
}
None => "Curve Editor - Unnamed Curve".to_string(),
}
} else {
"Curve Editor".to_string()
};
ui.send(self.window, WindowMessage::Title(WindowTitle::text(title)));
}
fn revert(&self) {
if let Some(curve_resource) = self.curve_resource.as_ref() {
curve_resource.data_ref().curve = self.backup.clone();
}
}
fn open_save_file_dialog(&self, resource_manager: &ResourceManager, ui: &UserInterface) {
ui.send(
self.save_file_selector,
FileSelectorMessage::Root(Some(resource_manager.registry_folder())),
);
ui.send(
self.save_file_selector,
WindowMessage::Open {
alignment: WindowAlignment::Center,
modal: true,
focus_content: true,
},
);
}
pub fn handle_ui_message(mut self, message: &UiMessage, engine: &mut Engine) -> Option<Self> {
let ui = &engine.user_interfaces.first_mut();
if let Some(ButtonMessage::Click) = message.data() {
if message.destination() == self.cancel {
if self.modified && self.curve_resource.is_some() {
ui.send(
self.cancel_message_box,
MessageBoxMessage::Open {
text: None,
title: None,
},
);
} else {
self.destroy(ui);
return None;
}
} else if message.destination() == self.ok {
if self.modified && self.curve_resource.is_some() {
if self.path == PathBuf::default() {
ui.send(
self.save_changes_message_box,
MessageBoxMessage::Open {
text: None,
title: None,
},
);
} else {
self.save();
self.destroy(ui);
return None;
}
} else {
self.destroy(ui);
return None;
}
}
} else if let Some(CurveEditorMessage::Sync(curve)) = message.data_from(self.curve_editor) {
if let Some(curve_resource) = self.curve_resource.as_ref() {
self.command_stack.do_command(
Command::new(ModifyCurveCommand {
curve_resource: curve_resource.clone(),
curve: curve.first().cloned().unwrap(),
}),
&mut CurveEditorContext {},
);
self.modified = true;
}
} else if let Some(MenuItemMessage::Click) = message.data() {
if message.destination() == self.menu.edit.undo {
self.command_stack.undo(&mut CurveEditorContext {});
self.sync_to_model(ui);
} else if message.destination() == self.menu.edit.redo {
self.command_stack.redo(&mut CurveEditorContext {});
self.sync_to_model(ui);
} else if message.destination() == self.menu.file.load {
ui.send(
self.load_file_selector,
FileSelectorMessage::Root(Some(engine.resource_manager.registry_folder())),
);
ui.send(
self.load_file_selector,
WindowMessage::Open {
alignment: WindowAlignment::Center,
modal: true,
focus_content: true,
},
);
} else if message.destination() == self.menu.file.new {
self.path = Default::default();
self.set_curve(
&engine.resource_manager,
Resource::new_embedded(CurveResourceState::default()),
ui,
);
} else if message.destination() == self.menu.file.save {
if self.path == PathBuf::default() {
self.open_save_file_dialog(&engine.resource_manager, ui);
} else {
self.save();
}
}
} else if let Some(FileSelectorMessage::Commit(path)) = message.data() {
if message.destination() == self.load_file_selector {
if let Ok(curve) =
block_on(engine.resource_manager.request::<CurveResourceState>(path))
{
self.path.clone_from(path);
self.set_curve(&engine.resource_manager, curve, ui);
}
} else if message.destination() == self.save_file_selector {
self.path.clone_from(path);
self.save();
}
} else if let Some(MessageBoxMessage::Close(result)) =
message.data_from(self.save_changes_message_box)
{
match result {
MessageBoxResult::No => {
self.revert();
self.destroy(ui);
return None;
}
MessageBoxResult::Yes => {
if self.path == PathBuf::default() {
self.open_save_file_dialog(&engine.resource_manager, ui);
} else {
self.save();
self.destroy(ui);
return None;
}
}
_ => (),
}
} else if let Some(MessageBoxMessage::Close(MessageBoxResult::Yes)) =
message.data_from(self.cancel_message_box)
{
self.revert();
self.destroy(ui);
return None;
}
Some(self)
}
}
#[derive(Default)]
pub struct CurveEditorPlugin {
curve_editor_window: Option<CurveEditorWindow>,
open_curve_editor: Handle<MenuItem>,
}
impl CurveEditorPlugin {
pub const CURVE_EDITOR: Uuid = uuid!("20705d17-741d-45bf-a9ba-ec3cee34ac2b");
fn on_open_curve_editor_clicked(&mut self, editor: &mut Editor) {
let ui = editor.engine.user_interfaces.first_mut();
let ctx = &mut ui.build_ctx();
let curve_editor = self
.curve_editor_window
.get_or_insert_with(|| CurveEditorWindow::new(ctx));
curve_editor.open(ui);
}
}
impl EditorPlugin for CurveEditorPlugin {
fn on_start(&mut self, editor: &mut Editor) {
let ui = editor.engine.user_interfaces.first_mut();
let ctx = &mut ui.build_ctx();
self.open_curve_editor = create_menu_item("Curve Editor", Self::CURVE_EDITOR, vec![], ctx);
ui.send(
editor.menu.utils_menu.menu,
MenuItemMessage::AddItem(self.open_curve_editor),
);
}
fn on_ui_message(&mut self, message: &mut UiMessage, editor: &mut Editor) {
if let Some(MenuItemMessage::Click) = message.data() {
if message.destination() == self.open_curve_editor {
self.on_open_curve_editor_clicked(editor)
}
}
let curve_editor = some_or_return!(self.curve_editor_window.take());
self.curve_editor_window = curve_editor.handle_ui_message(message, &mut editor.engine);
}
}