mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 14:18:11 +08:00
Add the File > Export dialog and PNG/JPG downloading (#629)
* Add export dialog * Code review changes * More code review feedback * Fix compilation on stable Rust * Fixes to problems Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
e3e506ecfb
commit
060182fd31
@@ -1,12 +1,16 @@
|
||||
use crate::message_prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::NewDocumentDialogUpdate;
|
||||
use super::{ExportDialogUpdate, NewDocumentDialogUpdate};
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, Dialog)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum DialogMessage {
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
ExportDialog(ExportDialogUpdate),
|
||||
|
||||
#[remain::unsorted]
|
||||
#[child]
|
||||
NewDocumentDialog(NewDocumentDialogUpdate),
|
||||
@@ -23,5 +27,6 @@ pub enum DialogMessage {
|
||||
RequestComingSoonDialog {
|
||||
issue: Option<i32>,
|
||||
},
|
||||
RequestExportDialog,
|
||||
RequestNewDocumentDialog,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use super::*;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DialogMessageHandler {
|
||||
export_dialog: Export,
|
||||
new_document_dialog: NewDocument,
|
||||
}
|
||||
|
||||
@@ -15,6 +16,8 @@ impl MessageHandler<DialogMessage, (&BuildMetadata, &PortfolioMessageHandler)> f
|
||||
fn process_action(&mut self, message: DialogMessage, (build_metadata, portfolio): (&BuildMetadata, &PortfolioMessageHandler), responses: &mut VecDeque<Message>) {
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
#[remain::unsorted]
|
||||
DialogMessage::ExportDialog(message) => self.export_dialog.process_action(message, (), responses),
|
||||
#[remain::unsorted]
|
||||
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_action(message, (), responses),
|
||||
|
||||
@@ -44,6 +47,37 @@ impl MessageHandler<DialogMessage, (&BuildMetadata, &PortfolioMessageHandler)> f
|
||||
coming_soon.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "Warning".to_string() }.into());
|
||||
}
|
||||
DialogMessage::RequestExportDialog => {
|
||||
let artboard_handler = &portfolio.active_document().artboard_message_handler;
|
||||
let mut index = 0;
|
||||
let artboards = artboard_handler
|
||||
.artboard_ids
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|&artboard| artboard_handler.artboards_graphene_document.layer(&[artboard]).ok().map(|layer| (artboard, layer)))
|
||||
.map(|(artboard, layer)| {
|
||||
(
|
||||
artboard,
|
||||
format!(
|
||||
"Artboard: {}",
|
||||
layer.name.clone().unwrap_or_else(|| {
|
||||
index += 1;
|
||||
format!("Untitled {index}")
|
||||
})
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.export_dialog = Export {
|
||||
file_name: portfolio.active_document().name.clone(),
|
||||
scale_factor: 1.,
|
||||
artboards,
|
||||
..Default::default()
|
||||
};
|
||||
self.export_dialog.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
responses.push_back(FrontendMessage::DisplayDialog { icon: "File".to_string() }.into());
|
||||
}
|
||||
DialogMessage::RequestNewDocumentDialog => {
|
||||
self.new_document_dialog = NewDocument {
|
||||
name: portfolio.generate_new_document_name(),
|
||||
@@ -56,5 +90,5 @@ impl MessageHandler<DialogMessage, (&BuildMetadata, &PortfolioMessageHandler)> f
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(DialogMessageDiscriminant;RequestNewDocumentDialog,CloseAllDocumentsWithConfirmation);
|
||||
advertise_actions!(DialogMessageDiscriminant;RequestNewDocumentDialog,RequestExportDialog,CloseAllDocumentsWithConfirmation);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::layout::layout_message::LayoutTarget;
|
||||
use crate::layout::widgets::*;
|
||||
use crate::message_prelude::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A dialog to allow users to customise their file export.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Export {
|
||||
pub file_name: String,
|
||||
pub file_type: FileType,
|
||||
pub scale_factor: f64,
|
||||
pub bounds: ExportBounds,
|
||||
pub artboards: HashMap<LayerId, String>,
|
||||
}
|
||||
|
||||
impl PropertyHolder for Export {
|
||||
fn properties(&self) -> WidgetLayout {
|
||||
let file_name = vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "File Name".into(),
|
||||
table_align: true,
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||
value: self.file_name.clone(),
|
||||
on_update: WidgetCallback::new(|text_input: &TextInput| ExportDialogUpdate::FileName(text_input.value.clone()).into()),
|
||||
})),
|
||||
];
|
||||
|
||||
let entries = [(FileType::Svg, "SVG"), (FileType::Png, "PNG"), (FileType::Jpg, "JPG")]
|
||||
.into_iter()
|
||||
.map(|(val, name)| RadioEntryData {
|
||||
label: name.into(),
|
||||
on_update: WidgetCallback::new(move |_| ExportDialogUpdate::FileType(val).into()),
|
||||
..RadioEntryData::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let export_type = vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "File Type".into(),
|
||||
table_align: true,
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::RadioInput(RadioInput {
|
||||
selected_index: self.file_type as u32,
|
||||
entries,
|
||||
})),
|
||||
];
|
||||
|
||||
let artboards = self.artboards.iter().map(|(&val, name)| (ExportBounds::Artboard(val), name.to_string()));
|
||||
let mut export_area_options = vec![(ExportBounds::AllArtwork, "All Artwork".to_string())];
|
||||
export_area_options.extend(artboards);
|
||||
let index = export_area_options.iter().position(|(val, _)| val == &self.bounds).unwrap();
|
||||
let menu_entries = vec![export_area_options
|
||||
.into_iter()
|
||||
.map(|(val, name)| DropdownEntryData {
|
||||
label: name,
|
||||
on_update: WidgetCallback::new(move |_| ExportDialogUpdate::ExportBounds(val).into()),
|
||||
..Default::default()
|
||||
})
|
||||
.collect()];
|
||||
|
||||
let export_area = vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Bounds".into(),
|
||||
table_align: true,
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::DropdownInput(DropdownInput {
|
||||
selected_index: index as u32,
|
||||
menu_entries,
|
||||
..Default::default()
|
||||
})),
|
||||
];
|
||||
|
||||
let resolution = vec![
|
||||
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Scale Factor".into(),
|
||||
table_align: true,
|
||||
..TextLabel::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: self.scale_factor,
|
||||
label: "".into(),
|
||||
unit: " ".into(),
|
||||
disabled: self.file_type == FileType::Svg,
|
||||
min: Some(0.),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| ExportDialogUpdate::ScaleFactor(number_input.value).into()),
|
||||
..NumberInput::default()
|
||||
})),
|
||||
];
|
||||
|
||||
let button_widgets = vec![
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "OK".to_string(),
|
||||
min_width: 96,
|
||||
emphasized: true,
|
||||
on_update: WidgetCallback::new(|_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followup: Box::new(ExportDialogUpdate::Submit.into()),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
..Default::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::TextButton(TextButton {
|
||||
label: "Cancel".to_string(),
|
||||
min_width: 96,
|
||||
on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogDismiss.into()),
|
||||
..Default::default()
|
||||
})),
|
||||
];
|
||||
|
||||
WidgetLayout::new(vec![
|
||||
LayoutRow::Row {
|
||||
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||
value: "Export".to_string(),
|
||||
bold: true,
|
||||
..Default::default()
|
||||
}))],
|
||||
},
|
||||
LayoutRow::Row { widgets: file_name },
|
||||
LayoutRow::Row { widgets: export_type },
|
||||
LayoutRow::Row { widgets: resolution },
|
||||
LayoutRow::Row { widgets: export_area },
|
||||
LayoutRow::Row { widgets: button_widgets },
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[impl_message(Message, DialogMessage, ExportDialog)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum ExportDialogUpdate {
|
||||
FileName(String),
|
||||
FileType(FileType),
|
||||
ScaleFactor(f64),
|
||||
ExportBounds(ExportBounds),
|
||||
|
||||
Submit,
|
||||
}
|
||||
|
||||
impl MessageHandler<ExportDialogUpdate, ()> for Export {
|
||||
fn process_action(&mut self, action: ExportDialogUpdate, _data: (), responses: &mut VecDeque<Message>) {
|
||||
match action {
|
||||
ExportDialogUpdate::FileName(name) => self.file_name = name,
|
||||
ExportDialogUpdate::FileType(export_type) => self.file_type = export_type,
|
||||
ExportDialogUpdate::ScaleFactor(x) => self.scale_factor = x,
|
||||
ExportDialogUpdate::ExportBounds(export_area) => self.bounds = export_area,
|
||||
|
||||
ExportDialogUpdate::Submit => responses.push_front(
|
||||
DocumentMessage::ExportDocument {
|
||||
file_name: self.file_name.clone(),
|
||||
file_type: self.file_type,
|
||||
scale_factor: self.scale_factor,
|
||||
bounds: self.bounds,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
|
||||
self.register_properties(responses, LayoutTarget::DialogDetails);
|
||||
}
|
||||
|
||||
advertise_actions! {ExportDialogUpdate;}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ mod close_all_documents_dialog;
|
||||
mod close_document_dialog;
|
||||
mod coming_soon_dialog;
|
||||
mod error_dialog;
|
||||
mod export_dialog;
|
||||
mod new_document_dialog;
|
||||
|
||||
pub use about_dialog::AboutGraphite;
|
||||
@@ -10,4 +11,5 @@ pub use close_all_documents_dialog::CloseAllDocuments;
|
||||
pub use close_document_dialog::CloseDocument;
|
||||
pub use coming_soon_dialog::ComingSoon;
|
||||
pub use error_dialog::Error;
|
||||
pub use export_dialog::{Export, ExportDialogUpdate, ExportDialogUpdateDiscriminant};
|
||||
pub use new_document_dialog::{NewDocument, NewDocumentDialogUpdate, NewDocumentDialogUpdateDiscriminant};
|
||||
|
||||
Reference in New Issue
Block a user