aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/actions.rs23
-rw-r--r--src/dialogs.rs98
-rw-r--r--src/handlers/export.rs26
-rw-r--r--src/handlers/mod.rs1
-rw-r--r--src/main.rs10
5 files changed, 152 insertions, 6 deletions
diff --git a/src/actions.rs b/src/actions.rs
index 119df04..efb9f53 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -1,5 +1,27 @@
use std::path::PathBuf;
+#[derive(Debug, Clone, Copy)]
+pub enum ImageFormat {
+ Png,
+ Svg,
+}
+
+impl ImageFormat {
+ pub fn extension(&self) -> &'static str {
+ match self {
+ ImageFormat::Png => "png",
+ ImageFormat::Svg => "svg",
+ }
+ }
+
+ pub fn mime_type(&self) -> &'static str {
+ match self {
+ ImageFormat::Png => "image/png",
+ ImageFormat::Svg => "image/svg+xml",
+ }
+ }
+}
+
#[derive(Debug)]
pub enum Action {
ChangeOrientation,
@@ -9,6 +31,7 @@ pub enum Action {
ZoomOut,
ZoomIn,
ExportImage,
+ ExportToFile { path: PathBuf, format: ImageFormat },
New,
Open,
Save { close_after: bool },
diff --git a/src/dialogs.rs b/src/dialogs.rs
index 6b906ed..752f99d 100644
--- a/src/dialogs.rs
+++ b/src/dialogs.rs
@@ -6,7 +6,7 @@ use gtk::prelude::*;
use relm4::{ComponentSender, gtk};
use crate::AppModel;
-use crate::actions::Action;
+use crate::actions::{Action, ImageFormat};
use crate::constants;
use crate::file_registry;
@@ -156,3 +156,99 @@ pub fn show_unsaved_changes_dialog(
});
dialog.show();
}
+
+/// Creates a file filter for an image format.
+fn create_image_filter(format: ImageFormat) -> gtk::FileFilter {
+ let filter = gtk::FileFilter::new();
+ filter.add_pattern(&format!("*.{}", format.extension()));
+ filter.add_mime_type(format.mime_type());
+ let name = match format {
+ ImageFormat::Png => "PNG Image",
+ ImageFormat::Svg => "SVG Image",
+ };
+ filter.set_name(Some(name));
+ filter
+}
+
+/// Shows an export image dialog and handles the response.
+pub fn show_export_dialog(
+ window: &gtk::Window,
+ current_file: Option<PathBuf>,
+ sender: ComponentSender<AppModel>,
+) {
+ let dialog = gtk::FileChooserDialog::new(
+ Some("Export Map as Image"),
+ Some(window),
+ gtk::FileChooserAction::Save,
+ &[
+ ("Cancel", gtk::ResponseType::Cancel),
+ ("Export", gtk::ResponseType::Accept),
+ ],
+ );
+
+ let png_filter = create_image_filter(ImageFormat::Png);
+ let svg_filter = create_image_filter(ImageFormat::Svg);
+ dialog.add_filter(&png_filter);
+ dialog.add_filter(&svg_filter);
+
+ // Set default filename based on current file
+ let base_name = current_file
+ .as_ref()
+ .and_then(|p| p.file_stem())
+ .and_then(|s| s.to_str())
+ .unwrap_or("map")
+ .to_string();
+
+ if let Some(ref current_path) = current_file
+ && let Some(parent) = current_path.parent()
+ {
+ let folder = gtk::gio::File::for_path(parent);
+ dialog.set_current_folder(Some(&folder)).ok();
+ }
+ dialog.set_current_name(&format!("{}.png", base_name));
+
+ // Update extension when filter changes
+ {
+ let png_filter_ref = png_filter.clone();
+ let base_name_clone = base_name.clone();
+ dialog.connect_notify_local(Some("filter"), move |dialog, _| {
+ let ext = if dialog.filter().as_ref() == Some(&png_filter_ref) {
+ "png"
+ } else {
+ "svg"
+ };
+ dialog.set_current_name(&format!("{}.{}", base_name_clone, ext));
+ });
+ }
+
+ let png_filter_clone = png_filter.clone();
+ dialog.connect_response(move |dialog, response| {
+ dialog.close();
+ if response == gtk::ResponseType::Accept
+ && let Some(file) = dialog.file()
+ && let Some(path) = file.path()
+ {
+ // Determine format from selected filter or file extension
+ let format = if dialog.filter().as_ref() == Some(&png_filter_clone) {
+ ImageFormat::Png
+ } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
+ match ext.to_lowercase().as_str() {
+ "svg" => ImageFormat::Svg,
+ _ => ImageFormat::Png,
+ }
+ } else {
+ ImageFormat::Png
+ };
+
+ // Ensure correct extension
+ let path = if path.extension().is_none() {
+ path.with_extension(format.extension())
+ } else {
+ path
+ };
+
+ sender.input(Action::ExportToFile { path, format });
+ }
+ });
+ dialog.present();
+}
diff --git a/src/handlers/export.rs b/src/handlers/export.rs
new file mode 100644
index 0000000..7438ef9
--- /dev/null
+++ b/src/handlers/export.rs
@@ -0,0 +1,26 @@
+use std::path::PathBuf;
+
+use crate::AppModel;
+use crate::actions::ImageFormat;
+use crate::dialogs;
+use wmap_renderer::{render_to_png, render_to_svg};
+
+/// Exports the map to an image file in the specified format.
+pub fn export_to_file(model: &AppModel, path: PathBuf, format: ImageFormat) {
+ let result: Result<(), String> = match format {
+ ImageFormat::Png => {
+ render_to_png(&model.map, model.stage_type, &model.render_configuration)
+ .map_err(|e| e.to_string())
+ .and_then(|data| std::fs::write(&path, data).map_err(|e| e.to_string()))
+ }
+ ImageFormat::Svg => {
+ render_to_svg(&model.map, model.stage_type, &model.render_configuration)
+ .map_err(|e| e.to_string())
+ .and_then(|data| std::fs::write(&path, data).map_err(|e| e.to_string()))
+ }
+ };
+
+ if let Err(error) = result {
+ dialogs::show_error(&model.window, "Error Exporting Image", &error);
+ }
+}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index 7d7cfec..168feb8 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -1,3 +1,4 @@
+pub mod export;
pub mod file;
pub mod view;
pub mod zoom;
diff --git a/src/main.rs b/src/main.rs
index 30f2381..2162845 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -500,12 +500,12 @@ impl SimpleComponent for AppModel {
Action::ZoomIn => handlers::zoom::zoom_in(self),
Action::ZoomOut => handlers::zoom::zoom_out(self),
- // Export (not yet implemented)
+ // Export
Action::ExportImage => {
- // TODO: Implement image export functionality
- // - Show save dialog with PNG/SVG format picker
- // - Default filename: current file name with .png or .svg extension
- // - Call renderer's render_to_png() or render_to_svg() based on selection
+ dialogs::show_export_dialog(&self.window, self.current_file.clone(), sender)
+ }
+ Action::ExportToFile { path, format } => {
+ handlers::export::export_to_file(self, path, format)
}
// File actions