aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorRubén Beltrán del Río <jj@r.bdr.sh>2026-03-26 18:13:49 +0100
committerRubén Beltrán del Río <jj@r.bdr.sh>2026-03-26 23:02:28 +0100
commitc5d45b748d90c79f881c4e054b425d68801dad14 (patch)
treeb39ddf37fb74d761b98d4e0e8f2abdb1a764273e /src/main.rs
parent5f5b94474c83f85d95be5c53d148c4205c17d8e1 (diff)
Spin header into its own component
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs377
1 files changed, 81 insertions, 296 deletions
diff --git a/src/main.rs b/src/main.rs
index e1d3bc9..c9393f4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -26,6 +26,7 @@ mod i18n;
mod preferences;
mod stages;
mod ui_helpers;
+mod components;
mod icon_names {
#![allow(clippy::doc_markdown)] // Generated code from relm4_icons_build
@@ -33,29 +34,53 @@ mod icon_names {
}
use std::cell::RefCell;
+use std::convert::identity;
use std::path::PathBuf;
use crate::actions::Action;
-use crate::icon_names::shipped::{
- IMAGE_REGULAR, MENU_LARGE, SPLIT_HORIZONTAL_REGULAR, SPLIT_VERTICAL_REGULAR, ZOOM_IN_REGULAR,
- ZOOM_OUT_REGULAR,
-};
+use crate::icon_names::shipped::{ ZOOM_IN_REGULAR, ZOOM_OUT_REGULAR };
use cairo::ImageSurface;
-use gtk::gio;
use gtk::glib;
-use relm4::{
- Component, ComponentController, ComponentParts, ComponentSender, RelmApp, RelmWidgetExt,
- SimpleComponent,
- actions::{AccelsPlus, ActionablePlus, RelmAction, RelmActionGroup},
- gtk, adw
-};
+use relm4::prelude::*;
+use relm4::actions::{AccelsPlus, ActionablePlus, RelmAction, RelmActionGroup};
use adw::prelude::*;
+
use sourceview5::LanguageManager;
use sourceview5::prelude::*;
-use stages::{ALL_STAGE_TYPES, LocalizedStageType};
+
+use stages::LocalizedStageType;
use wmap_parser::{Map, parse};
use wmap_renderer::{Configuration, StageType, render_to_surface};
+use components::header::{Header, HeaderInit};
+
+/// Macro to reduce boilerplate when registering actions with accelerators.
+macro_rules! register_action {
+ ($group:expr, $app:expr, $sender:expr, $action_type:ty, $message:expr, $accelerators:expr) => {{
+ let sender = $sender.clone();
+ let action: RelmAction<$action_type> = RelmAction::new_stateless(move |_| {
+ let _ = sender.input($message);
+ });
+ $group.add_action(action.clone());
+ $app.set_accelerators_for_action::<$action_type>($accelerators);
+ action
+ }};
+}
+
+/// Macro to further reduce the boilerplate, by allowing terser registration.
+macro_rules! register_actions {
+ ($group:expr, $app:expr, $sender:expr, [
+ $( $action_type:ty => $message:expr, $accelerators:expr );+ $(;)?
+ ]) => {{
+ $(
+ register_action!(
+ $group, $app, $sender,
+ $action_type, $message, $accelerators
+ );
+ )+
+ }};
+}
+
// Action group and action type declarations
relm4::new_action_group!(WindowActionGroup, "window");
relm4::new_stateless_action!(NewAction, WindowActionGroup, "new");
@@ -82,18 +107,6 @@ impl relm4::actions::ActionName for NewFromTemplateAction {
const NAME: &'static str = "new-from-template";
}
-/// Macro to reduce boilerplate when registering actions with accelerators.
-macro_rules! register_action {
- ($group:expr, $app:expr, $sender:expr, $action_type:ty, $message:expr, $accelerators:expr) => {{
- let sender = $sender.clone();
- let action: RelmAction<$action_type> = RelmAction::new_stateless(move |_| {
- sender.input($message);
- });
- $group.add_action(action.clone());
- $app.set_accelerators_for_action::<$action_type>($accelerators);
- action
- }};
-}
struct AppInit {
zoom: f64,
@@ -103,8 +116,6 @@ struct AppInit {
struct AppModel {
orientation: gtk::Orientation,
- stage_type_list: gtk::StringList,
- available_stage_types: Vec<StageType>,
stage_type: StageType,
source: sourceview5::Buffer,
source_view: sourceview5::View,
@@ -123,10 +134,9 @@ struct AppModel {
preferences: preferences::UserPreferences,
preferences_window: Option<relm4::Controller<preferences::PreferencesWindow>>,
editor_css_provider: gtk::CssProvider,
- templates_menu: gio::Menu,
- main_menu: gio::Menu,
horizontal_layout_enabled: bool,
- layout_action: RelmAction<ChangeOrientationAction>
+ // Child Components
+ header: Controller<Header>
}
impl AppModel {
@@ -222,40 +232,6 @@ impl AppModel {
}
}
- fn rebuild_stage_type_list(&mut self) {
- let mut stage_types: Vec<StageType> = ALL_STAGE_TYPES.to_vec();
-
- for custom in &self.preferences.custom_stages {
- stage_types.push(StageType::Custom {
- name: custom.name.clone(),
- i: custom.stage.i.clone(),
- ii: custom.stage.ii.clone(),
- iii: custom.stage.iii.clone(),
- iv: custom.stage.iv.clone(),
- });
- }
-
- while self.stage_type_list.n_items() > 0 {
- self.stage_type_list.remove(0);
- }
-
- for stage_type in &stage_types {
- self.stage_type_list.append(&stage_type.localized_name());
- }
-
- self.available_stage_types = stage_types;
- }
-
- fn rebuild_templates_menu(&self) {
- self.templates_menu.remove_all();
- for template in &self.preferences.map_templates {
- self.templates_menu.append(
- Some(&template.name),
- Some(&format!("window.new-from-template::{}", template.id)),
- );
- }
- }
-
pub fn open_new_window(file_path: Option<PathBuf>, initial_content: Option<String>) {
let controller = AppModel::builder()
.launch(AppInit {
@@ -273,18 +249,6 @@ impl AppModel {
file_registry::store_controller(controller);
}
- fn init_stage_type_list() -> gtk::StringList {
- let stage_types: Vec<String> = ALL_STAGE_TYPES
- .iter()
- .map(stages::LocalizedStageType::localized_name)
- .collect();
- let stage_type_refs: Vec<&str> = stage_types
- .iter()
- .map(std::string::String::as_str)
- .collect();
- gtk::StringList::new(&stage_type_refs)
- }
-
fn init_source_buffer(
init: &AppInit,
sender: &ComponentSender<Self>,
@@ -342,51 +306,6 @@ impl AppModel {
(drawing_area, map, render_configuration, stage_type, surface)
}
- fn init_main_menu(prefs: &preferences::UserPreferences) -> (gio::Menu, gio::Menu) {
- let templates_menu = gio::Menu::new();
- for template in &prefs.map_templates {
- templates_menu.append(
- Some(&template.name),
- Some(&format!("window.new-from-template::{}", template.id)),
- );
- }
-
- let main_menu = gio::Menu::new();
- main_menu.append(Some(&tr!("command.file.new")), Some("window.new"));
- main_menu.append_submenu(
- Some(&tr!("command.file.new_from_template")),
- &templates_menu,
- );
- main_menu.append(Some(&tr!("command.file.open")), Some("window.open"));
- main_menu.append(Some(&tr!("command.file.save")), Some("window.save"));
- main_menu.append(Some(&tr!("command.file.save_as")), Some("window.save-as"));
- main_menu.append(Some(&tr!("command.file.close")), Some("window.close"));
- main_menu.append(
- Some(&tr!("command.file.export")),
- Some("window.export-image"),
- );
-
- let layout_section = gio::Menu::new();
- layout_section.append(
- Some(&tr!("command.view.use_vertical_layout")),
- Some("window.change-orientation"),
- );
- main_menu.append_section(None, &layout_section);
-
- let zoom_section = gio::Menu::new();
- zoom_section.append(Some(&tr!("command.view.zoom_in")), Some("window.zoom-in"));
- zoom_section.append(Some(&tr!("command.view.zoom_out")), Some("window.zoom-out"));
- main_menu.append_section(None, &zoom_section);
-
- let prefs_section = gio::Menu::new();
- prefs_section.append(
- Some(&tr!("command.application.preferences")),
- Some("window.preferences"),
- );
- main_menu.append_section(None, &prefs_section);
-
- (templates_menu, main_menu)
- }
}
#[relm4::component]
@@ -403,49 +322,8 @@ impl SimpleComponent for AppModel {
set_default_width: constants::DEFAULT_WINDOW_WIDTH,
set_default_height: constants::DEFAULT_WINDOW_HEIGHT,
- gtk::Box {
- set_orientation: gtk::Orientation::Vertical,
- set_spacing: 0,
-
- adw::HeaderBar {
- pack_start = &adw::ToolbarView {
- gtk::Button::with_label(&match model.orientation {
- gtk::Orientation::Horizontal => tr!("command.view.use_vertical_layout"),
- _ => tr!("command.view.use_horizontal_layout")
- }) {
- #[watch]
- set_icon_name: match model.orientation {
- gtk::Orientation::Horizontal => SPLIT_VERTICAL_REGULAR,
- _ => SPLIT_HORIZONTAL_REGULAR
- },
- #[watch]
- set_visible: model.horizontal_layout_enabled,
- ActionablePlus::set_stateless_action::<ChangeOrientationAction>: &(),
- }
- },
- pack_end = &adw::ToolbarView {
-
- gtk::Button::with_label(&tr!("command.file.export")) {
- set_icon_name: IMAGE_REGULAR,
- ActionablePlus::set_stateless_action::<ExportImageAction>: &(),
- },
-
- gtk::DropDown {
- set_model: Some(&model.stage_type_list),
- set_show_arrow: true,
- set_selected: 0,
- connect_selected_notify[sender] => move |dropdown| {
- let selected = dropdown.selected();
- sender.input(Action::StageTypeSelected(selected as usize));
- },
- },
- gtk::MenuButton {
- set_icon_name: MENU_LARGE,
- #[wrap(Some)]
- set_popover = &gtk::PopoverMenu::from_model(Some(&model.main_menu)) {}
- }
- }
- },
+ adw::ToolbarView {
+ add_top_bar = model.header.widget(),
gtk::Paned {
#[watch]
@@ -477,7 +355,7 @@ impl SimpleComponent for AppModel {
}
},
- gtk::ActionBar {
+ add_bottom_bar = &gtk::ActionBar {
pack_end = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
@@ -528,7 +406,6 @@ impl SimpleComponent for AppModel {
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
- let stage_type_list = Self::init_stage_type_list();
let (source, initial_content, current_file) = Self::init_source_buffer(&init, &sender);
@@ -552,30 +429,26 @@ impl SimpleComponent for AppModel {
file_registry::register_file(path, &root);
}
- let prefs = preferences::storage::load();
- let (templates_menu, main_menu) = Self::init_main_menu(&prefs);
+ let preferences = preferences::storage::load();
- // Actions
+ let orientation = gtk::Orientation::Horizontal;
+ let horizontal_layout_enabled = false;
+ // Actions
let application = relm4::main_application();
let mut action_group = RelmActionGroup::<WindowActionGroup>::new();
- register_action!(
- action_group,
- application,
- sender,
- ZoomInAction,
- Action::ZoomIn,
- &["<Primary>equal", "<Primary>plus"]
- );
- register_action!(
- action_group,
- application,
- sender,
- ZoomOutAction,
- Action::ZoomOut,
- &["<Primary>minus"]
- );
+ register_actions!(action_group, application, sender, [
+ ZoomInAction => Action::ZoomIn, &["<Primary>equal", "<Primary>plus"];
+ ZoomOutAction => Action::ZoomOut, &["<Primary>minus"];
+ ExportImageAction => Action::ExportImage, &["<Primary>e"];
+ NewAction => Action::New, &["<Primary>n"];
+ SaveAction => Action::Save { close_after: false }, &["<Primary>s"];
+ SaveAsAction => Action::SaveAs { close_after: false }, &["<Primary><Shift>s"];
+ OpenAction => Action::Open, &["<Primary>o"];
+ CloseAction => Action::CloseWindow, &["<Primary>w"];
+ PreferencesAction => Action::ShowPreferences, &["<Primary>comma"]
+ ]);
let layout_action = register_action!(
action_group,
application,
@@ -584,62 +457,6 @@ impl SimpleComponent for AppModel {
Action::ChangeOrientation,
&["<Primary>l"]
);
- register_action!(
- action_group,
- application,
- sender,
- ExportImageAction,
- Action::ExportImage,
- &["<Primary>e"]
- );
- register_action!(
- action_group,
- application,
- sender,
- NewAction,
- Action::New,
- &["<Primary>n"]
- );
- register_action!(
- action_group,
- application,
- sender,
- SaveAction,
- Action::Save { close_after: false },
- &["<Primary>s"]
- );
- register_action!(
- action_group,
- application,
- sender,
- SaveAsAction,
- Action::SaveAs { close_after: false },
- &["<Primary><Shift>s"]
- );
- register_action!(
- action_group,
- application,
- sender,
- OpenAction,
- Action::Open,
- &["<Primary>o"]
- );
- register_action!(
- action_group,
- application,
- sender,
- CloseAction,
- Action::CloseWindow,
- &["<Primary>w"]
- );
- register_action!(
- action_group,
- application,
- sender,
- PreferencesAction,
- Action::ShowPreferences,
- &["<Primary>comma"]
- );
let sender_clone = sender.clone();
let action: RelmAction<NewFromTemplateAction> =
@@ -649,10 +466,20 @@ impl SimpleComponent for AppModel {
action_group.add_action(action);
action_group.register_for_widget(&root);
+
+ // Child Components
+ let header = Header::builder()
+ .launch(HeaderInit {
+ layout_action,
+ custom_stages: preferences.custom_stages.clone(),
+ templates: preferences.map_templates.clone(),
+ orientation,
+ horizontal_layout_enabled
+ })
+ .forward(sender.input_sender(), identity);
+
let mut model = AppModel {
- orientation: gtk::Orientation::Horizontal,
- stage_type_list,
- available_stage_types: ALL_STAGE_TYPES.to_vec(),
+ orientation,
stage_type,
source,
source_view: source_view.clone(),
@@ -668,17 +495,14 @@ impl SimpleComponent for AppModel {
pending_load_events: RefCell::new(0),
initialized: false,
window: root.clone(),
- preferences: prefs,
+ preferences,
preferences_window: None,
editor_css_provider,
- templates_menu,
- main_menu,
horizontal_layout_enabled: true,
- layout_action
+ header
};
model.apply_preferences();
- model.rebuild_stage_type_list();
model.update_image();
file_registry::register_sender(sender.input_sender().clone());
@@ -723,7 +547,7 @@ impl SimpleComponent for AppModel {
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
match message {
Action::ChangeOrientation => handlers::view::change_orientation(self),
- Action::StageTypeSelected(index) => handlers::view::stage_type_selected(self, index),
+ Action::StageTypeSelected(stage_type) => handlers::view::stage_type_selected(self, stage_type),
Action::SourceChanged => {
let pending = *self.pending_load_events.borrow();
@@ -785,54 +609,15 @@ impl SimpleComponent for AppModel {
Action::MarkInitialized => self.initialized = true,
// Layout Transition
- Action::DisableHorizontalLayout => {
- self.horizontal_layout_enabled = false;
- self.layout_action.set_enabled(false);
- },
- Action::EnableHorizontalLayout => {
- self.horizontal_layout_enabled = true;
- self.layout_action.set_enabled(true);
- },
+ Action::DisableHorizontalLayout => handlers::view::change_horizontal_layout(self, false),
+ Action::EnableHorizontalLayout => handlers::view::change_horizontal_layout(self, 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.rebuild_stage_type_list();
- self.rebuild_templates_menu();
- self.update_image();
- file_registry::broadcast_to_all_windows(&Action::ReloadPreferences);
- }
- Action::PreferencesWindowClosed => {
- self.preferences_window = None;
- }
- Action::ReloadPreferences => {
- self.preferences = preferences::storage::load();
- self.apply_preferences();
- self.rebuild_stage_type_list();
- self.rebuild_templates_menu();
- self.update_image();
- }
+ Action::ShowPreferences => handlers::preferences::show(self, &sender),
+ Action::PreferencesChanged(preferences) => handlers::preferences::update(self, preferences),
+ Action::PreferencesWindowClosed => handlers::preferences::close(self),
+ Action::ReloadPreferences => handlers::preferences::reload(self),
}
}
}
@@ -842,8 +627,8 @@ fn main() {
relm4_icons::initialize_icons(icon_names::GRESOURCE_BYTES, icon_names::RESOURCE_PREFIX);
- let prefs = preferences::storage::load();
- let initial_content = prefs
+ let preferences = preferences::storage::load();
+ let initial_content = preferences
.map_templates
.iter()
.find(|t| t.is_default)