use crate::{
define_command_stack, send_sync_message, utils::create_file_selector, MessageBoxButtons,
MessageBoxMessage, MSG_SYNC_FLAG,
};
use fyrox::{
asset::{Resource, ResourceData, ResourceState},
core::{
color::Color, curve::Curve, futures::executor::block_on, pool::Handle, visitor::prelude::*,
visitor::Visitor,
},
engine::Engine,
gui::{
border::BorderBuilder,
brush::Brush,
button::{ButtonBuilder, ButtonMessage},
curve::{CurveEditorBuilder, CurveEditorMessage},
file_browser::{FileBrowserMode, FileSelectorMessage},
grid::{Column, GridBuilder, Row},
menu::{MenuBuilder, MenuItemBuilder, MenuItemContent, MenuItemMessage},
message::{MessageDirection, UiMessage},
messagebox::{MessageBoxBuilder, MessageBoxResult},
stack_panel::StackPanelBuilder,
widget::{WidgetBuilder, WidgetMessage},
window::{WindowBuilder, WindowMessage, WindowTitle},
BuildContext, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
},
resource::curve::{CurveResource, CurveResourceState},
};
use std::{fmt::Debug, path::PathBuf};
#[derive(Debug)]
pub struct CurveEditorContext {}
define_command_stack!(CurveCommand, CurveCommandStack, 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 CurveCommand for ModifyCurveCommand {
fn name(&mut self, _: &CurveEditorContext) -> String {
"Modify Curve".to_owned()
}
fn execute(&mut self, _: &mut CurveEditorContext) {
self.swap();
}
fn revert(&mut self, _: &mut CurveEditorContext) {
self.swap();
}
}
struct FileMenu {
new: Handle<UiNode>,
save: Handle<UiNode>,
load: Handle<UiNode>,
}
struct EditMenu {
undo: Handle<UiNode>,
redo: Handle<UiNode>,
}
struct Menu {
file: FileMenu,
edit: EditMenu,
}
pub struct CurveEditorWindow {
window: Handle<UiNode>,
curve_editor: Handle<UiNode>,
ok: Handle<UiNode>,
cancel: Handle<UiNode>,
curve_resource: Option<CurveResource>,
command_stack: CurveCommandStack,
menu: Menu,
load_file_selector: Handle<UiNode>,
save_file_selector: Handle<UiNode>,
path: PathBuf,
save_changes_message_box: Handle<UiNode>,
cancel_message_box: Handle<UiNode>,
modified: bool,
backup: Curve,
}
impl CurveEditorWindow {
pub fn new(ctx: &mut BuildContext) -> Self {
let load_file_selector = create_file_selector(ctx, "crv", FileBrowserMode::Open);
let save_file_selector = create_file_selector(
ctx,
"crv",
FileBrowserMode::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(Brush::Solid(Color::opaque(20, 20, 20)))
.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_title(WindowTitle::text("Curve Editor"))
.build(ctx);
Self {
window,
curve_editor,
ok,
cancel,
curve_resource: None,
command_stack: CurveCommandStack::new(false),
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 close(&mut self, ui: &UserInterface) {
self.clear(ui);
ui.send_message(WindowMessage::close(
self.window,
MessageDirection::ToWidget,
));
}
pub fn open(&self, ui: &UserInterface) {
ui.send_message(WindowMessage::open_modal(
self.window,
MessageDirection::ToWidget,
true,
));
}
fn sync_to_model(&mut self, ui: &UserInterface) {
if let Some(curve_resource) = self.curve_resource.as_ref() {
send_sync_message(
ui,
CurveEditorMessage::sync(
self.curve_editor,
MessageDirection::ToWidget,
curve_resource.data_ref().curve.clone(),
),
);
}
}
fn save(&self) {
if let Some(curve_resource) = self.curve_resource.as_ref() {
if let ResourceState::Ok(ref mut state) = *curve_resource.state() {
let mut visitor = Visitor::new();
state.curve.visit("Curve", &mut visitor).unwrap();
visitor.save_binary(&self.path).unwrap();
}
}
}
fn set_curve(&mut self, curve: CurveResource, ui: &UserInterface) {
self.backup = curve.data_ref().curve.clone();
self.curve_resource = Some(curve);
ui.send_message(WidgetMessage::enabled(
self.curve_editor,
MessageDirection::ToWidget,
true,
));
self.sync_to_model(ui);
self.sync_title(ui);
self.modified = false;
self.command_stack.clear(CurveEditorContext {});
}
fn sync_title(&self, ui: &UserInterface) {
let title = if let Some(curve_resource) = self.curve_resource.as_ref() {
let path = curve_resource.data_ref().path().to_path_buf();
if path == PathBuf::default() {
"Curve Editor - Unnamed Curve".to_string()
} else {
format!("Curve Editor - {}", path.display())
}
} else {
"Curve Editor".to_string()
};
ui.send_message(WindowMessage::title(
self.window,
MessageDirection::ToWidget,
WindowTitle::text(title),
));
}
fn clear(&mut self, ui: &UserInterface) {
self.path = Default::default();
self.backup = Default::default();
self.command_stack.clear(CurveEditorContext {});
self.curve_resource = None;
self.sync_title(ui);
ui.send_message(WidgetMessage::enabled(
self.curve_editor,
MessageDirection::ToWidget,
false,
));
send_sync_message(
ui,
CurveEditorMessage::sync(
self.curve_editor,
MessageDirection::ToWidget,
Default::default(),
),
);
}
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, ui: &UserInterface) {
ui.send_message(FileSelectorMessage::root(
self.save_file_selector,
MessageDirection::ToWidget,
Some(std::env::current_dir().unwrap()),
));
ui.send_message(WindowMessage::open_modal(
self.save_file_selector,
MessageDirection::ToWidget,
true,
));
}
pub fn handle_ui_message(&mut self, message: &UiMessage, engine: &mut Engine) {
let ui = &engine.user_interface;
if let Some(ButtonMessage::Click) = message.data() {
if message.destination() == self.cancel {
if self.modified && self.curve_resource.is_some() {
ui.send_message(MessageBoxMessage::open(
self.cancel_message_box,
MessageDirection::ToWidget,
None,
None,
));
} else {
self.close(ui);
}
} else if message.destination() == self.ok {
if self.modified && self.curve_resource.is_some() {
if self.path == PathBuf::default() {
ui.send_message(MessageBoxMessage::open(
self.save_changes_message_box,
MessageDirection::ToWidget,
None,
None,
));
} else {
self.save();
self.close(ui);
}
} else {
self.close(ui);
}
}
} else if let Some(CurveEditorMessage::Sync(curve)) = message.data() {
if message.destination() == self.curve_editor
&& message.direction() == MessageDirection::FromWidget
&& message.flags != MSG_SYNC_FLAG
{
if let Some(curve_resource) = self.curve_resource.as_ref() {
self.command_stack.do_command(
Box::new(ModifyCurveCommand {
curve_resource: curve_resource.clone(),
curve: curve.clone(),
}),
CurveEditorContext {},
);
self.modified = true;
}
}
} else if let Some(MenuItemMessage::Click) = message.data() {
if message.destination() == self.menu.edit.undo {
self.command_stack.undo(CurveEditorContext {});
self.sync_to_model(ui);
} else if message.destination() == self.menu.edit.redo {
self.command_stack.redo(CurveEditorContext {});
self.sync_to_model(ui);
} else if message.destination() == self.menu.file.load {
ui.send_message(FileSelectorMessage::root(
self.load_file_selector,
MessageDirection::ToWidget,
Some(std::env::current_dir().unwrap()),
));
ui.send_message(WindowMessage::open_modal(
self.load_file_selector,
MessageDirection::ToWidget,
true,
));
} else if message.destination() == self.menu.file.new {
self.path = Default::default();
self.set_curve(
CurveResource(Resource::new(ResourceState::Ok(
CurveResourceState::default(),
))),
ui,
);
} else if message.destination() == self.menu.file.save {
if self.path == PathBuf::default() {
self.open_save_file_dialog(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_curve(path)) {
self.path = path.clone();
self.set_curve(curve, ui);
}
} else if message.destination() == self.save_file_selector {
self.path = path.clone();
self.save();
}
} else if let Some(MessageBoxMessage::Close(result)) = message.data() {
if message.destination() == self.save_changes_message_box {
match result {
MessageBoxResult::No => {
self.revert();
self.close(ui);
}
MessageBoxResult::Yes => {
if self.path == PathBuf::default() {
self.open_save_file_dialog(ui);
} else {
self.save();
self.close(ui);
}
}
_ => (),
}
} else if message.destination() == self.cancel_message_box {
if let MessageBoxResult::Yes = result {
self.revert();
self.close(ui);
}
}
}
}
}