aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorRubén Beltrán del Río <jj@r.bdr.sh>2026-01-16 08:26:35 +0100
committerRubén Beltrán del Río <jj@r.bdr.sh>2026-01-16 11:57:13 +0100
commit81cd06b3334f2f91f6a1ab62a28e11c7d0c677eb (patch)
treef0b0cf37b406eda2dec8db4382aeb5f606cd1f0d /src/main.rs
parent4211b2ae06777d5bbe8261a1ab5c0dd057829a35 (diff)
Add preferences
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs128
1 files changed, 124 insertions, 4 deletions
diff --git a/src/main.rs b/src/main.rs
index 2162845..6a7bd34 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,12 @@
+// Suppress warnings from relm4 view! macro internals
+#![allow(unused_assignments)]
+
mod actions;
mod constants;
mod dialogs;
mod file_registry;
mod handlers;
+mod preferences;
mod stages;
mod icon_names {
@@ -47,6 +51,7 @@ relm4::new_stateless_action!(
);
relm4::new_stateless_action!(ZoomInAction, WindowActionGroup, "zoom-in");
relm4::new_stateless_action!(ZoomOutAction, WindowActionGroup, "zoom-out");
+relm4::new_stateless_action!(PreferencesAction, WindowActionGroup, "preferences");
/// Macro to reduce boilerplate when registering actions with accelerators.
macro_rules! register_action {
@@ -136,6 +141,14 @@ fn setup_actions(window: gtk::Window, sender: ComponentSender<AppModel>) {
Action::CloseWindow,
&["<Primary>w"]
);
+ register_action!(
+ action_group,
+ application,
+ sender,
+ PreferencesAction,
+ Action::ShowPreferences,
+ &["<Primary>comma"]
+ );
action_group.register_for_widget(&window);
}
@@ -150,6 +163,7 @@ struct AppModel {
stage_type_list: gtk::StringList,
stage_type: StageType,
source: sourceview5::Buffer,
+ source_view: sourceview5::View,
map: Map,
zoom: f64,
render_configuration: Configuration,
@@ -162,12 +176,15 @@ struct AppModel {
pending_load_events: RefCell<u32>,
initialized: bool,
window: gtk::Window,
+ preferences: preferences::UserPreferences,
+ preferences_window: Option<relm4::Controller<preferences::PreferencesWindow>>,
+ editor_css_provider: gtk::CssProvider,
}
impl AppModel {
fn update_image(&mut self) {
if let Ok(surface) =
- render_to_surface(&self.map, self.stage_type, &self.render_configuration)
+ render_to_surface(&self.map, &self.stage_type, &self.render_configuration)
{
self.surface_width = (surface.width() as f64 * self.zoom).round() as i32;
self.surface_height = (surface.height() as f64 * self.zoom).round() as i32;
@@ -217,6 +234,48 @@ impl AppModel {
self.source.set_text(content);
}
+ fn apply_preferences(&mut self) {
+ // Apply editor preferences
+ self.source_view
+ .set_wrap_mode(if self.preferences.soft_wrap_lines {
+ gtk::WrapMode::Word
+ } else {
+ gtk::WrapMode::None
+ });
+
+ // Build CSS for editor font
+ let css = if self.preferences.use_custom_editor_font {
+ self.source_view.set_monospace(false);
+ format!(
+ "textview {{ font-family: \"{}\"; font-size: {}pt; }}",
+ self.preferences.custom_editor_font_name, self.preferences.editor_font_size as i32
+ )
+ } else {
+ // Use monospace but still apply font size
+ self.source_view.set_monospace(true);
+ format!(
+ "textview {{ font-size: {}pt; }}",
+ self.preferences.editor_font_size as i32
+ )
+ };
+
+ // Update the CSS provider (reusing the same one avoids accumulation)
+ self.editor_css_provider.load_from_data(&css);
+
+ // Apply render configuration
+ self.render_configuration.options.smart_label_positioning =
+ self.preferences.use_smart_label_positioning;
+ self.render_configuration.options.show_background = self.preferences.show_map_background;
+
+ // Apply map font
+ if self.preferences.use_custom_font {
+ self.render_configuration.theme.fonts.face = self.preferences.custom_font_name.clone();
+ } else {
+ // Reset to default font
+ self.render_configuration.theme.fonts.face = String::from("sans-serif");
+ }
+ }
+
pub fn open_new_window(file_path: Option<PathBuf>) {
let controller = AppModel::builder()
.launch(AppInit {
@@ -297,9 +356,9 @@ impl SimpleComponent for AppModel {
#[wrap(Some)]
set_start_child = &gtk::ScrolledWindow {
- sourceview5::View {
+ #[local_ref]
+ source_view -> sourceview5::View {
set_expand: true,
- set_monospace: true,
set_buffer: Some(&model.source)
}
},
@@ -380,6 +439,9 @@ impl SimpleComponent for AppModel {
"Zoom In" => ZoomInAction,
"Zoom Out" => ZoomOutAction,
},
+ section! {
+ "Preferences" => PreferencesAction,
+ },
}
}
@@ -422,13 +484,24 @@ impl SimpleComponent for AppModel {
setup_actions(root.clone(), sender.clone());
+ // Create source view
+ let source_view = sourceview5::View::with_buffer(&source);
+ source_view.set_monospace(true);
+
+ // Create CSS provider for editor font styling
+ let editor_css_provider = gtk::CssProvider::new();
+ source_view.style_context().add_provider(
+ &editor_css_provider,
+ gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
+ );
+
let drawing_area = gtk::DrawingArea::new();
let map = parse(&initial_content);
let mut render_configuration = Configuration::default();
let stage_type = StageType::Activities;
render_configuration.options.smart_label_positioning = true;
let surface =
- render_to_surface(&map, stage_type, &render_configuration).unwrap_or_else(|_| {
+ render_to_surface(&map, &stage_type, &render_configuration).unwrap_or_else(|_| {
ImageSurface::create(cairo::Format::ARgb32, 1, 1)
.expect("Failed to create fallback surface")
});
@@ -437,11 +510,15 @@ impl SimpleComponent for AppModel {
file_registry::register_file(path, &root);
}
+ // Load preferences
+ let prefs = preferences::storage::load();
+
let mut model = AppModel {
orientation: gtk::Orientation::Horizontal,
stage_type_list,
stage_type,
source,
+ source_view: source_view.clone(),
map,
zoom: init.zoom,
render_configuration,
@@ -454,7 +531,13 @@ impl SimpleComponent for AppModel {
pending_load_events: RefCell::new(0),
initialized: false,
window: root.clone(),
+ preferences: prefs,
+ preferences_window: None,
+ editor_css_provider,
};
+
+ // Apply preferences
+ model.apply_preferences();
model.update_image();
{
@@ -528,6 +611,43 @@ impl SimpleComponent for AppModel {
// Internal actions
Action::MarkInitialized => self.initialized = true,
+
+ // Preferences
+ Action::ShowPreferences => {
+ if self.preferences_window.is_none() {
+ let controller = preferences::PreferencesWindow::builder()
+ .transient_for(&self.window)
+ .launch(self.preferences.clone())
+ .forward(sender.input_sender(), |output| match output {
+ preferences::window::PreferencesOutput::PreferencesChanged(prefs) => {
+ Action::PreferencesChanged(prefs)
+ }
+ preferences::window::PreferencesOutput::Closed => {
+ Action::PreferencesWindowClosed
+ }
+ });
+ self.preferences_window = Some(controller);
+ }
+ if let Some(ref controller) = self.preferences_window {
+ controller.widget().present();
+ }
+ }
+ Action::PreferencesChanged(prefs) => {
+ self.preferences = prefs;
+ self.apply_preferences();
+ self.update_image();
+ // Broadcast to all other windows to reload preferences
+ file_registry::broadcast_to_all_windows(Action::ReloadPreferences);
+ }
+ Action::PreferencesWindowClosed => {
+ self.preferences_window = None;
+ }
+ Action::ReloadPreferences => {
+ // Reload preferences from disk (another window changed them)
+ self.preferences = preferences::storage::load();
+ self.apply_preferences();
+ self.update_image();
+ }
}
}
}