diff options
| author | Rubén Beltrán del Río <jj@r.bdr.sh> | 2026-01-16 00:36:23 +0100 |
|---|---|---|
| committer | Rubén Beltrán del Río <jj@r.bdr.sh> | 2026-01-16 00:58:35 +0100 |
| commit | 4211b2ae06777d5bbe8261a1ab5c0dd057829a35 (patch) | |
| tree | 15d501e3c101abfa5f6657b023f385e4272fbcc4 /src | |
| parent | 04b1e6080d99601b8d6343be3140f545cd8f9eae (diff) | |
Add export image
Diffstat (limited to 'src')
| -rw-r--r-- | src/actions.rs | 23 | ||||
| -rw-r--r-- | src/dialogs.rs | 98 | ||||
| -rw-r--r-- | src/handlers/export.rs | 26 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 1 | ||||
| -rw-r--r-- | src/main.rs | 10 |
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: >k::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 |