// Map, wardley map editor for linux // Copyright (C) 2026 Rubén Beltrán del Río // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use gtk::prelude::*; use relm4::prelude::*; use crate::tr; use crate::actions::PreferencesAction; use crate::components::switch; const DEFAULT_FONT_SIZE: f64 = 14.0; pub struct EditorInit { pub font_size: f64, pub soft_wrap: bool, pub use_custom_font: bool, pub custom_font: String, } pub struct Editor { soft_wrap_switch: Controller, custom_font_switch: Controller, soft_wrap: bool, use_custom_font: bool, font_size: f64, custom_font: String, } #[derive(Debug, Clone)] pub enum EditorAction { SetFontSize(f64), SetSoftWrap(bool), SetUseCustomFont(bool), SetCustomFont(String), // Internal ChangeFontSize(f64), ChangeSoftWrap(bool), ChangeUseCustomFont(bool), ChangeFontDescription(gtk::FontDialogButton), } #[relm4::component(pub)] impl SimpleComponent for Editor { type Init = EditorInit; type Input = EditorAction; type Output = PreferencesAction; view! { gtk::Box { set_orientation: gtk::Orientation::Vertical, set_margin_all: 20, set_spacing: 20, set_valign: gtk::Align::Start, gtk::Box { set_orientation: gtk::Orientation::Vertical, set_spacing: 10, gtk::Label { set_label: &tr!("preferences.editor.font_size.title"), add_css_class: "heading", set_halign: gtk::Align::Start, }, // TODO: Remove magic numbers. gtk::Scale::with_range(gtk::Orientation::Horizontal, 7.0, 21.0, 1.0) { #[watch] #[block_signal(size_handler)] set_value: model.font_size, set_draw_value: true, set_hexpand: true, set_format_value_func => |_, value| { #[allow(clippy::cast_possible_truncation)] #[allow(clippy::float_cmp)] if value == DEFAULT_FONT_SIZE { format!( "{} ({})", value as i32, tr!("preferences.editor.default_font_size") ) } else { format!("{}", value as i32) } }, connect_value_changed[sender] => move |scale| { sender.input(EditorAction::ChangeFontSize(scale.value())); } @size_handler, } }, gtk::Label { set_label: &tr!("preferences.editor.editor_style.title"), add_css_class: "heading", set_halign: gtk::Align::Start, set_margin_top: 10, }, model.soft_wrap_switch.widget(), model.custom_font_switch.widget(), gtk::Box { set_orientation: gtk::Orientation::Horizontal, set_spacing: 10, gtk::Label { set_label: &tr!("preferences.editor.font"), }, gtk::FontDialogButton::new(Some(gtk::FontDialog::new())) { #[watch] #[block_signal(font_handler)] set_font_desc: >k::pango::FontDescription::from_string(&format!( "{} {}", model.custom_font, model.font_size as i32 )), #[watch] set_sensitive: model.use_custom_font, set_hexpand: true, set_use_font: true, set_use_size: false, connect_font_desc_notify[sender] => move |button| { sender.input(EditorAction::ChangeFontDescription(button.clone())); } @font_handler, } } } } fn init( init: Self::Init, root: Self::Root, sender: ComponentSender, ) -> ComponentParts { let soft_wrap_switch = switch::Switch::builder() .launch(switch::SwitchInit { label: tr!("preferences.editor.editor_style.soft_wrap_lines"), state: init.soft_wrap, }) .forward(sender.input_sender(), move |output| match output { switch::SwitchOutput::SetState(state) => EditorAction::ChangeSoftWrap(state), }); let custom_font_switch = switch::Switch::builder() .launch(switch::SwitchInit { label: tr!("preferences.editor.editor_style.use_custom_font"), state: init.use_custom_font, }) .forward(sender.input_sender(), move |output| match output { switch::SwitchOutput::SetState(state) => EditorAction::ChangeUseCustomFont(state), }); let model = Self { font_size: init.font_size, soft_wrap: init.soft_wrap, use_custom_font: init.use_custom_font, custom_font: init.custom_font, soft_wrap_switch, custom_font_switch, }; let widgets = view_output!(); ComponentParts { model, widgets } } fn update(&mut self, message: Self::Input, sender: ComponentSender) { match message { EditorAction::SetFontSize(font_size) => self.font_size = font_size, EditorAction::SetSoftWrap(soft_wrap) => self.soft_wrap = soft_wrap, EditorAction::SetUseCustomFont(use_custom_font) => { self.use_custom_font = use_custom_font } EditorAction::SetCustomFont(custom_font) => self.custom_font = custom_font, EditorAction::ChangeFontSize(size) => { sender .output(PreferencesAction::SetEditorFontSize(size)) .ok(); } EditorAction::ChangeFontDescription(button) => { if let Some(font) = button.font_desc() { if let Some(family) = font.family() { sender .output(PreferencesAction::SetCustomEditorFontName( family.to_string(), )) .ok(); } let size = font.size() / gtk::pango::SCALE; if size > 0 { sender .output(PreferencesAction::SetEditorFontSize(f64::from(size))) .ok(); } } } EditorAction::ChangeSoftWrap(state) => { sender .output(PreferencesAction::SetSoftWrapLines(state)) .ok(); } EditorAction::ChangeUseCustomFont(state) => { sender .output(PreferencesAction::SetUseCustomEditorFont(state)) .ok(); } } } }