// 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();
}
}
}
}