mod constants; mod stages; mod icon_names { include!(concat!(env!("OUT_DIR"), "/icon_names.rs")); } use wmap_parser::{parse, Map}; use wmap_renderer::{render_to_surface, StageType, Configuration}; use gtk::prelude::*; use relm4::{gtk, ComponentParts, ComponentSender, RelmApp, RelmWidgetExt, SimpleComponent}; use sourceview5::prelude::{BufferExt, TextBufferExt, TextViewExt}; use sourceview5::LanguageManager; use stages::all_stages; use crate::icon_names::shipped::{ZOOM_IN_REGULAR, ZOOM_OUT_REGULAR, IMAGE_REGULAR, PLUS, SPLIT_HORIZONTAL_REGULAR, SPLIT_VERTICAL_REGULAR}; use cairo::ImageSurface; struct AppModel { orientation: gtk::Orientation, stage_type_list: gtk::StringList, stage_type: StageType, source: sourceview5::Buffer, map: Map, zoom: f64, render_configuration: Configuration, surface: ImageSurface, surface_width: i32, surface_height: i32, drawing_area: gtk::DrawingArea, } #[derive(Debug)] enum AppAction { ChangeOrientation, StageTypeSelected(usize), SourceChanged, Zoom(f64), ZoomOut, ZoomIn, } impl AppModel { fn update_image(&mut self) { if let Ok(surface) = 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; self.surface = surface; let zoom = self.zoom; let surface_clone = self.surface.clone(); self.drawing_area.set_draw_func(move |_, cr, _width, _height| { cr.scale(zoom, zoom); cr.set_source_surface(&surface_clone, 0.0, 0.0).ok(); cr.paint().ok(); }); self.drawing_area.set_content_width(self.surface_width); self.drawing_area.set_content_height(self.surface_height); self.drawing_area.queue_draw(); } } } #[relm4::component] impl SimpleComponent for AppModel { /// The type of the messages that this component can receive. type Input = AppAction; /// The type of the messages that this component can send. type Output = (); /// The type of data with which this component will be initialized. type Init = f64; view! { gtk::Window { set_title: Some("Map"), set_default_width: 300, set_default_height: 100, gtk::Box { set_orientation: gtk::Orientation::Vertical, set_spacing: 0, gtk::HeaderBar { pack_start = >k::Box { set_orientation: gtk::Orientation::Horizontal, gtk::Button::with_label("New") { set_icon_name: PLUS }, gtk::Button::with_label("Change Orientation") { #[watch] set_icon_name: match model.orientation { gtk::Orientation::Horizontal => SPLIT_VERTICAL_REGULAR, _ => SPLIT_HORIZONTAL_REGULAR }, connect_clicked => AppAction::ChangeOrientation } }, pack_end = >k::Box { set_orientation: gtk::Orientation::Horizontal, gtk::Button::with_label("Print") { set_icon_name: IMAGE_REGULAR }, 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(AppAction::StageTypeSelected(selected as usize)); }, } } }, gtk::Paned { #[watch] set_orientation: model.orientation, set_expand: true, #[wrap(Some)] set_start_child = >k::ScrolledWindow { sourceview5::View { set_expand: true, set_monospace: true, set_buffer: Some(&model.source) } }, #[wrap(Some)] set_end_child = >k::ScrolledWindow { #[local_ref] drawing_area -> gtk::DrawingArea { #[watch] set_content_width: model.surface_width, #[watch] set_content_height: model.surface_height } } // #[wrap(Some)] // set_end_child = >k::DrawingArea { // set_expand: true // } }, gtk::ActionBar { pack_end = >k::Box { set_orientation: gtk::Orientation::Horizontal, gtk::Label { #[watch] set_label: &format!("{:.1}x", model.zoom), set_margin_end: 5 }, gtk::Button::with_label("Zoom Out") { set_icon_name: ZOOM_OUT_REGULAR, set_margin_end: 5, add_css_class: "flat", add_css_class: "zoom", add_css_class: "zoom-out", connect_clicked => AppAction::ZoomOut }, gtk::Scale { set_orientation: gtk::Orientation::Horizontal, #[watch] set_value: model.zoom, set_draw_value: false, set_range: (constants::MIN_ZOOM, constants::MAX_ZOOM), set_increments: (constants::ZOOM_STEP, constants::ZOOM_STEP), set_value_pos: gtk::PositionType::Left, set_width_request: 100, connect_value_changed[sender] => move |scale| { sender.input(AppAction::Zoom(scale.value())); } }, gtk::Button::with_label("Zoom In") { set_icon_name: ZOOM_IN_REGULAR, add_css_class: "flat", add_css_class: "zoom", add_css_class: "zoom-in", connect_clicked => AppAction::ZoomIn }, } } } } } /// Initialize the UI and model. fn init( zoom: Self::Init, root: Self::Root, sender: ComponentSender, ) -> ComponentParts { let stage_types: Vec<&str> = all_stages() .iter() .map(|stage_type| stage_type.name()) .collect(); let stage_type_list = gtk::StringList::new(&stage_types); let source = sourceview5::Buffer::new(None); let cloned_sender = sender.clone(); source.connect_changed(move |_| { cloned_sender.input(AppAction::SourceChanged); }); // Enable syntax highlighting source.set_highlight_syntax(true); // Set default language let language_manager = LanguageManager::default(); if let Some(language) = language_manager.language("wmap") { source.set_language(Some(&language)); } let drawing_area = gtk::DrawingArea::new(); let model = AppModel { orientation: gtk::Orientation::Horizontal, stage_type_list, stage_type: StageType::Practice, source, map: parse(""), zoom, render_configuration: Configuration::default(), surface: ImageSurface::create(cairo::Format::ARgb32, 0, 0).unwrap(), surface_width: 0, surface_height: 0, drawing_area: drawing_area.clone(), }; let widgets = view_output!(); ComponentParts { model, widgets } } fn update(&mut self, message: Self::Input, _sender: ComponentSender) { match message { AppAction::ChangeOrientation => { self.orientation = match self.orientation { gtk::Orientation::Horizontal => gtk::Orientation::Vertical, _ => gtk::Orientation::Horizontal, } } AppAction::StageTypeSelected(index) => { let all_stages = all_stages(); if index < all_stages.len() { self.stage_type = all_stages[index]; } self.update_image(); } AppAction::SourceChanged => { let text = self.source .text(&self.source.start_iter(), &self.source.end_iter(), false); self.map = parse(&text); self.update_image(); } AppAction::Zoom(new_zoom) => { self.zoom = new_zoom; self.update_image(); }, AppAction::ZoomOut => { if self.zoom - constants::ZOOM_STEP >= constants::MIN_ZOOM { self.zoom -= constants::ZOOM_STEP; self.update_image(); } } AppAction::ZoomIn => { if self.zoom < constants::MAX_ZOOM { self.zoom += constants::ZOOM_STEP; self.update_image(); } } } } } fn main() { relm4_icons::initialize_icons(icon_names::GRESOURCE_BYTES, icon_names::RESOURCE_PREFIX); let app = RelmApp::new("relm4.test.simple_manual"); app.run::(1.0); }