Skip to main content

rustapi/
editor.rs

1use crate::Embedded;
2use crate::prelude::*;
3#[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
4use crate::self_update::{SelfUpdateEvent, SelfUpdater};
5use codegridfx::Module;
6use rusterix::render_settings::RendererBackend;
7use rusterix::server::message::AudioCommand;
8use rusterix::{
9    PlayerCamera, Rusterix, SceneManager, SceneManagerResult, Texture, Value, ValueContainer,
10};
11use shared::rusterix_utils::*;
12use std::fs;
13use std::path::PathBuf;
14use std::str::FromStr;
15use std::sync::mpsc::Receiver;
16#[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
17use std::sync::{
18    Arc, Mutex,
19    mpsc::{Sender, channel},
20};
21
22#[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
23use std::thread;
24
25pub static PREVIEW_ICON: LazyLock<RwLock<(TheRGBATile, i32)>> =
26    LazyLock::new(|| RwLock::new((TheRGBATile::default(), 0)));
27
28pub static SIDEBARMODE: LazyLock<RwLock<SidebarMode>> =
29    LazyLock::new(|| RwLock::new(SidebarMode::Region));
30pub static UNDOMANAGER: LazyLock<RwLock<UndoManager>> =
31    LazyLock::new(|| RwLock::new(UndoManager::default()));
32pub static TOOLLIST: LazyLock<RwLock<ToolList>> =
33    LazyLock::new(|| RwLock::new(ToolList::default()));
34pub static ACTIONLIST: LazyLock<RwLock<ActionList>> =
35    LazyLock::new(|| RwLock::new(ActionList::default()));
36// pub static PANELS: LazyLock<RwLock<Panels>> = LazyLock::new(|| RwLock::new(Panels::new()));
37pub static PALETTE: LazyLock<RwLock<ThePalette>> =
38    LazyLock::new(|| RwLock::new(ThePalette::default()));
39pub static RUSTERIX: LazyLock<RwLock<Rusterix>> =
40    LazyLock::new(|| RwLock::new(Rusterix::default()));
41pub static CONFIGEDITOR: LazyLock<RwLock<ConfigEditor>> =
42    LazyLock::new(|| RwLock::new(ConfigEditor::new()));
43pub static CONFIG: LazyLock<RwLock<toml::Table>> =
44    LazyLock::new(|| RwLock::new(toml::Table::default()));
45pub static EDITCAMERA: LazyLock<RwLock<EditCamera>> =
46    LazyLock::new(|| RwLock::new(EditCamera::new()));
47pub static SCENEMANAGER: LazyLock<RwLock<SceneManager>> =
48    LazyLock::new(|| RwLock::new(SceneManager::default()));
49pub static DOCKMANAGER: LazyLock<RwLock<DockManager>> =
50    LazyLock::new(|| RwLock::new(DockManager::default()));
51
52pub static CODEGRIDFX: LazyLock<RwLock<Module>> =
53    LazyLock::new(|| RwLock::new(Module::as_type(codegridfx::ModuleType::CharacterTemplate)));
54
55#[derive(Clone)]
56struct ProjectSession {
57    project: Project,
58    project_path: Option<PathBuf>,
59    undo: UndoManager,
60    dirty: bool,
61}
62
63#[derive(Serialize, Deserialize, Clone, Debug, Default)]
64struct CreatorWindowState {
65    x: Option<i32>,
66    y: Option<i32>,
67    width: Option<usize>,
68    height: Option<usize>,
69}
70
71pub struct Editor {
72    project: Project,
73    project_path: Option<PathBuf>,
74    sessions: Vec<ProjectSession>,
75    active_session: usize,
76    replace_next_project_load_in_active_tab: bool,
77    last_active_dirty: bool,
78
79    sidebar: Sidebar,
80    mapeditor: MapEditor,
81
82    server_ctx: ServerContext,
83
84    update_tracker: UpdateTracker,
85    event_receiver: Option<Receiver<TheEvent>>,
86
87    #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
88    self_update_rx: Receiver<SelfUpdateEvent>,
89    #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
90    self_update_tx: Sender<SelfUpdateEvent>,
91    #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
92    self_updater: Arc<Mutex<SelfUpdater>>,
93
94    update_counter: usize,
95    last_processed_log_len: usize,
96
97    build_values: ValueContainer,
98    window_state: CreatorWindowState,
99}
100
101impl Editor {
102    fn log_segment_has_warning_or_error(segment: &str) -> bool {
103        segment.contains("[error]") || segment.contains("[warning]")
104    }
105
106    fn window_state_file_path() -> Option<PathBuf> {
107        let home = std::env::var("HOME").ok()?;
108        Some(
109            PathBuf::from(home)
110                .join(".eldiron")
111                .join("creator_window_state.json"),
112        )
113    }
114
115    fn load_window_state() -> CreatorWindowState {
116        if let Some(path) = Self::window_state_file_path()
117            && let Ok(data) = fs::read_to_string(path)
118            && let Ok(state) = serde_json::from_str::<CreatorWindowState>(&data)
119        {
120            return state;
121        }
122        CreatorWindowState::default()
123    }
124
125    fn save_window_state(&self) {
126        if let Some(path) = Self::window_state_file_path() {
127            if let Some(dir) = path.parent() {
128                let _ = fs::create_dir_all(dir);
129            }
130            if let Ok(json) = serde_json::to_string(&self.window_state) {
131                let _ = fs::write(path, json);
132            }
133        }
134    }
135
136    fn persist_active_region_view_state(&mut self) {
137        if let Some(region) = self.project.get_region_mut(&self.server_ctx.curr_region) {
138            match self.server_ctx.editor_view_mode {
139                EditorViewMode::Iso => {
140                    region.editing_position_iso_3d = Some(region.editing_position_3d);
141                    region.editing_look_at_iso_3d = Some(region.editing_look_at_3d);
142                    region.editing_iso_scale = Some(EDITCAMERA.read().unwrap().iso_camera.scale);
143                }
144                EditorViewMode::Orbit => {
145                    region.editing_position_orbit_3d = Some(region.editing_position_3d);
146                    region.editing_look_at_orbit_3d = Some(region.editing_look_at_3d);
147                    region.editing_orbit_distance =
148                        Some(EDITCAMERA.read().unwrap().orbit_camera.distance);
149                }
150                EditorViewMode::FirstP => {
151                    region.editing_position_firstp_3d = Some(region.editing_position_3d);
152                    region.editing_look_at_firstp_3d = Some(region.editing_look_at_3d);
153                }
154                EditorViewMode::D2 => {}
155            }
156        }
157    }
158
159    fn project_tab_title_for(
160        project: &Project,
161        project_path: &Option<PathBuf>,
162        fallback_index: usize,
163        dirty: bool,
164    ) -> String {
165        let prefix = if dirty { "* " } else { "" };
166
167        if let Some(path) = project_path
168            && let Some(stem) = path.file_stem()
169            && let Some(name) = stem.to_str()
170            && !name.is_empty()
171        {
172            return format!("{}{}", prefix, name);
173        }
174        if !project.name.is_empty() {
175            return format!("{}{}", prefix, project.name);
176        }
177
178        if project_path.is_none() {
179            return format!("{}{}", prefix, fl!("new_project"));
180        }
181
182        format!("{}Project {}", prefix, fallback_index + 1)
183    }
184
185    fn sync_active_session_from_editor(&mut self) {
186        if self.active_session >= self.sessions.len() {
187            return;
188        }
189        self.persist_active_region_view_state();
190        self.sessions[self.active_session].project = self.project.clone();
191        self.sessions[self.active_session].project_path = self.project_path.clone();
192        self.sessions[self.active_session].undo = UNDOMANAGER.read().unwrap().clone();
193        self.sessions[self.active_session].dirty = self.active_session_has_changes();
194    }
195
196    fn sync_editor_from_active_session(&mut self) {
197        if self.active_session >= self.sessions.len() {
198            return;
199        }
200        let session = self.sessions[self.active_session].clone();
201        self.project = session.project;
202        self.project_path = session.project_path;
203        *UNDOMANAGER.write().unwrap() = session.undo;
204    }
205
206    fn rebuild_project_tabs(&self, ui: &mut TheUI) {
207        if let Some(widget) = ui.get_widget("Project Tabs")
208            && let Some(tabbar) = widget.as_tabbar()
209        {
210            tabbar.clear();
211            for (index, session) in self.sessions.iter().enumerate() {
212                tabbar.add_tab(Self::project_tab_title_for(
213                    &session.project,
214                    &session.project_path,
215                    index,
216                    session.dirty,
217                ));
218            }
219            tabbar.set_selection_index(self.active_session);
220        }
221    }
222
223    fn activate_loaded_project(
224        &mut self,
225        ui: &mut TheUI,
226        ctx: &mut TheContext,
227        update_server_icons: &mut bool,
228        redraw: &mut bool,
229    ) {
230        self.update_counter = 0;
231        self.sidebar.startup = true;
232
233        if let Some(widget) = ui.get_widget("Server Time Slider") {
234            widget.set_value(TheValue::Time(self.project.time));
235        }
236
237        {
238            let mut rusterix = RUSTERIX.write().unwrap();
239            rusterix.client.set_server_time(self.project.time);
240            if rusterix.server.state == rusterix::ServerState::Running
241                && let Some(map) = self.project.get_map(&self.server_ctx)
242            {
243                rusterix.server.set_time(&map.id, self.project.time);
244            }
245        }
246
247        self.server_ctx.clear();
248        if let Some(first) = self.project.regions.first() {
249            self.server_ctx.curr_region = first.id;
250        }
251
252        self.sidebar
253            .load_from_project(ui, ctx, &mut self.server_ctx, &mut self.project);
254        self.mapeditor.load_from_project(ui, ctx, &self.project);
255        *update_server_icons = true;
256        *redraw = true;
257
258        *PALETTE.write().unwrap() = self.project.palette.clone();
259        SCENEMANAGER
260            .write()
261            .unwrap()
262            .set_palette(self.project.palette.clone());
263
264        UNDOMANAGER.read().unwrap().set_undo_state_to_ui(ctx);
265    }
266
267    fn switch_to_session(
268        &mut self,
269        index: usize,
270        ui: &mut TheUI,
271        ctx: &mut TheContext,
272        update_server_icons: &mut bool,
273        redraw: &mut bool,
274    ) {
275        if index >= self.sessions.len() {
276            self.rebuild_project_tabs(ui);
277            return;
278        }
279        if index == self.active_session {
280            self.sync_editor_from_active_session();
281            self.activate_loaded_project(ui, ctx, update_server_icons, redraw);
282            self.rebuild_project_tabs(ui);
283            return;
284        }
285        self.sync_active_session_from_editor();
286        self.active_session = index;
287        self.sync_editor_from_active_session();
288        self.activate_loaded_project(ui, ctx, update_server_icons, redraw);
289        self.rebuild_project_tabs(ui);
290    }
291
292    fn sanitize_loaded_project(project: &mut Project) {
293        insert_content_into_maps(project);
294
295        for r in &mut project.regions {
296            for s in &mut r.map.sectors {
297                if let Some(floor) = s.properties.get("floor_source") {
298                    s.properties.set("source", floor.clone());
299                }
300                if s.properties.contains("rect_rendering") {
301                    s.properties.set("rect", Value::Bool(true));
302                }
303                s.properties.remove("floor_source");
304                s.properties.remove("rect_rendering");
305                s.properties.remove("ceiling_source");
306            }
307        }
308
309        let mut char_names = FxHashMap::default();
310        for c in &project.characters {
311            char_names.insert(c.0, c.1.name.clone());
312        }
313        for r in &mut project.regions {
314            for c in &mut r.characters {
315                if let Some(n) = char_names.get(&c.1.character_id) {
316                    c.1.name = n.clone();
317                }
318            }
319        }
320
321        let mut item_names = FxHashMap::default();
322        for c in &project.items {
323            item_names.insert(c.0, c.1.name.clone());
324        }
325        for r in &mut project.regions {
326            for c in &mut r.items {
327                if let Some(n) = item_names.get(&c.1.item_id) {
328                    c.1.name = n.clone();
329                }
330            }
331            for (_, p) in &mut r.map.profiles {
332                p.sanitize();
333            }
334            r.map.sanitize();
335        }
336
337        for (_, screen) in &mut project.screens {
338            screen.map.sanitize();
339        }
340
341        if project.tiles.is_empty() {
342            let tiles = project.extract_tiles();
343            for (id, t) in &tiles {
344                let mut texture_array: Vec<Texture> = vec![];
345                for b in &t.buffer {
346                    let mut texture = Texture::new(
347                        b.pixels().to_vec(),
348                        b.dim().width as usize,
349                        b.dim().height as usize,
350                    );
351                    texture.generate_normals(true);
352                    texture_array.push(texture);
353                }
354                let tile = rusterix::Tile {
355                    id: t.id,
356                    role: rusterix::TileRole::from_index(t.role),
357                    textures: texture_array.clone(),
358                    module: None,
359                    blocking: t.blocking,
360                    scale: t.scale,
361                    tags: t.name.clone(),
362                };
363                project.tiles.insert(*id, tile);
364            }
365        }
366
367        for (_, tile) in project.tiles.iter_mut() {
368            for texture in &mut tile.textures {
369                texture.generate_normals(true);
370            }
371        }
372
373        for (_, character) in project.characters.iter_mut() {
374            if character.source.starts_with("class") {
375                character.source = character.module.build(false);
376                character.source_debug = character.module.build(true);
377            }
378        }
379
380        for (_, item) in project.items.iter_mut() {
381            if item.source.starts_with("class") {
382                item.source = item.module.build(false);
383                item.source_debug = item.module.build(true);
384            }
385        }
386    }
387
388    fn close_active_session(
389        &mut self,
390        ui: &mut TheUI,
391        ctx: &mut TheContext,
392        update_server_icons: &mut bool,
393        redraw: &mut bool,
394    ) {
395        if self.sessions.is_empty() {
396            return;
397        }
398
399        self.sync_active_session_from_editor();
400        self.sessions.remove(self.active_session);
401
402        if self.sessions.is_empty() {
403            let mut project = Project::default();
404            if let Some(bytes) = crate::Embedded::get("starter_project.eldiron")
405                && let Ok(project_string) = std::str::from_utf8(bytes.data.as_ref())
406                && let Ok(loaded) = serde_json::from_str(&project_string.to_string())
407            {
408                project = loaded;
409            }
410            Self::sanitize_loaded_project(&mut project);
411            self.sessions.push(ProjectSession {
412                project,
413                project_path: None,
414                undo: UndoManager::default(),
415                dirty: false,
416            });
417            self.active_session = 0;
418        } else if self.active_session >= self.sessions.len() {
419            self.active_session = self.sessions.len() - 1;
420        }
421
422        self.sync_editor_from_active_session();
423        self.activate_loaded_project(ui, ctx, update_server_icons, redraw);
424        self.rebuild_project_tabs(ui);
425    }
426
427    fn active_session_has_changes(&self) -> bool {
428        UNDOMANAGER.read().unwrap().has_unsaved() || DOCKMANAGER.read().unwrap().has_dock_changes()
429    }
430
431    fn is_realtime_mode(&self) -> bool {
432        self.server_ctx.game_mode
433            || RUSTERIX.read().unwrap().server.state == rusterix::ServerState::Running
434    }
435
436    fn redraw_interval_ms(&self) -> u64 {
437        let config = CONFIGEDITOR.read().unwrap();
438        if self.is_realtime_mode() {
439            (1000 / config.target_fps.clamp(1, 60)) as u64
440        } else {
441            config.game_tick_ms.max(1) as u64
442        }
443    }
444
445    fn help_url_for_data_context(&self) -> String {
446        match self.server_ctx.pc {
447            ProjectContext::ProjectSettings => "docs/configuration/game".to_string(),
448            ProjectContext::RegionSettings(_) => "docs/building_maps/region_settings".to_string(),
449            ProjectContext::CharacterPreviewRigging(_) => "docs/characters_items/rigging".into(),
450            ProjectContext::Character(_)
451            | ProjectContext::CharacterData(_)
452            | ProjectContext::Item(_)
453            | ProjectContext::ItemData(_) => "docs/characters_items/attributes".to_string(),
454            ProjectContext::Screen(_)
455            | ProjectContext::ScreenWidget(_, _)
456            | ProjectContext::RegionCharacterInstance(_, _)
457            | ProjectContext::RegionItemInstance(_, _) => "docs/screens/widgets".to_string(),
458            _ => "docs/creator/docks/attribute_editor".to_string(),
459        }
460    }
461
462    fn help_url_for_widget_name(&self, widget_name: &str) -> Option<String> {
463        match widget_name {
464            "Tiles" | "Tilemap" | "Tile Editor Dock RGBA Layout View" | "Tile Editor Tree" => {
465                Some("docs/creator/docks/tile_picker_editor".into())
466            }
467            "DockDataEditor" | "DockDataEditorMax" | "Data" => {
468                Some(self.help_url_for_data_context())
469            }
470            "DockCodeEditor" | "Code" => Some("docs/creator/docks/eldrin_script_editor".into()),
471            "Visual Code" => Some("docs/creator/docks/visual_script_editor".into()),
472            "PolyView" => {
473                if self.server_ctx.editor_view_mode == EditorViewMode::D2 {
474                    Some("docs/building_maps/creating_2d".into())
475                } else {
476                    Some("docs/building_maps/creating_3d_maps".into())
477                }
478            }
479            name if name.starts_with("DockVisualScripting") => {
480                Some("docs/creator/docks/visual_script_editor".into())
481            }
482            name if name.starts_with("Tile Editor ") => {
483                Some("docs/creator/docks/tile_picker_editor".into())
484            }
485            _ => None,
486        }
487    }
488
489    fn help_url_for_editor_event(&self, event: &TheEvent, ui: &mut TheUI) -> Option<String> {
490        let mut clicked = false;
491        let widget_name = match event {
492            TheEvent::StateChanged(id, state) if *state == TheWidgetState::Clicked => {
493                clicked = true;
494                Some(id.name.clone())
495            }
496            TheEvent::RenderViewClicked(id, _) => {
497                clicked = true;
498                Some(id.name.clone())
499            }
500            TheEvent::TilePicked(id, _) => {
501                clicked = true;
502                Some(id.name.clone())
503            }
504            TheEvent::TileEditorClicked(id, _) => {
505                clicked = true;
506                Some(id.name.clone())
507            }
508            TheEvent::MouseDown(coord) => {
509                clicked = true;
510                ui.get_widget_at_coord(*coord).map(|w| w.id().name.clone())
511            }
512            _ => None,
513        };
514
515        if let Some(widget_name) = widget_name
516            && let Some(url) = self.help_url_for_widget_name(&widget_name)
517        {
518            return Some(url);
519        }
520
521        if clicked {
522            let dm = DOCKMANAGER.read().unwrap();
523            if dm.state != DockManagerState::Minimized {
524                return match dm.dock.as_str() {
525                    "Tiles" => Some("docs/creator/docks/tile_picker_editor".into()),
526                    "Data" => Some(self.help_url_for_data_context()),
527                    "Code" => Some("docs/creator/docks/eldrin_script_editor".into()),
528                    "Visual Code" => Some("docs/creator/docks/visual_script_editor".into()),
529                    _ => None,
530                };
531            }
532        }
533        None
534    }
535}
536
537impl TheTrait for Editor {
538    fn new() -> Self
539    where
540        Self: Sized,
541    {
542        let mut project = Project::new();
543        if let Some(bytes) = crate::Embedded::get("toml/config.toml") {
544            if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
545                project.config = source.to_string();
546            }
547        }
548
549        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
550        let (self_update_tx, self_update_rx) = channel();
551
552        #[cfg(all(
553            not(target_arch = "wasm32"),
554            feature = "self-update",
555            not(target_os = "macos")
556        ))]
557        let self_updater = SelfUpdater::new("markusmoenig", "Eldiron", "eldiron-creator");
558        #[cfg(all(
559            not(target_arch = "wasm32"),
560            feature = "self-update",
561            target_os = "macos"
562        ))]
563        let self_updater = SelfUpdater::new("markusmoenig", "Eldiron", "Eldiron-Creator.app");
564
565        let initial_session = ProjectSession {
566            project: project.clone(),
567            project_path: None,
568            undo: UndoManager::default(),
569            dirty: false,
570        };
571
572        Self {
573            project,
574            project_path: None,
575            sessions: vec![initial_session],
576            active_session: 0,
577            replace_next_project_load_in_active_tab: false,
578            last_active_dirty: false,
579
580            sidebar: Sidebar::new(),
581            mapeditor: MapEditor::new(),
582
583            server_ctx: ServerContext::default(),
584
585            update_tracker: UpdateTracker::new(),
586            event_receiver: None,
587
588            #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
589            self_update_rx,
590            #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
591            self_update_tx,
592            #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
593            self_updater: Arc::new(Mutex::new(self_updater)),
594
595            update_counter: 0,
596            last_processed_log_len: 0,
597
598            build_values: ValueContainer::default(),
599            window_state: Self::load_window_state(),
600        }
601    }
602
603    fn init(&mut self, _ctx: &mut TheContext) {
604        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
605        {
606            let updater = Arc::clone(&self.self_updater);
607            let tx = self.self_update_tx.clone();
608
609            thread::spawn(move || {
610                let mut updater = updater.lock().unwrap();
611
612                if let Err(err) = updater.fetch_release_list() {
613                    tx.send(SelfUpdateEvent::UpdateError(err.to_string()))
614                        .unwrap();
615                };
616            });
617        }
618    }
619
620    fn window_title(&self) -> String {
621        "Eldiron Creator".to_string()
622    }
623
624    fn target_fps(&self) -> f64 {
625        1000.0 / self.redraw_interval_ms() as f64
626    }
627
628    fn fonts_to_load(&self) -> Vec<TheFontScript> {
629        vec![TheFontScript::Han]
630    }
631
632    fn default_window_size(&self) -> (usize, usize) {
633        (
634            self.window_state.width.unwrap_or(1200),
635            self.window_state.height.unwrap_or(720),
636        )
637    }
638
639    fn min_window_size(&self) -> (usize, usize) {
640        (1200, 720)
641    }
642
643    fn default_window_position(&self) -> Option<(i32, i32)> {
644        Some((self.window_state.x?, self.window_state.y?))
645    }
646
647    fn window_icon(&self) -> Option<(Vec<u8>, u32, u32)> {
648        if let Some(file) = Embedded::get("window_logo.png") {
649            let data = std::io::Cursor::new(file.data);
650
651            let decoder = png::Decoder::new(data);
652            if let Ok(mut reader) = decoder.read_info() {
653                if let Some(buffer_size) = reader.output_buffer_size() {
654                    let mut buf = vec![0; buffer_size];
655                    let info = reader.next_frame(&mut buf).unwrap();
656                    let bytes = &buf[..info.buffer_size()];
657
658                    Some((bytes.to_vec(), info.width, info.height))
659                } else {
660                    None
661                }
662            } else {
663                None
664            }
665        } else {
666            None
667        }
668    }
669
670    fn init_ui(&mut self, ui: &mut TheUI, ctx: &mut TheContext) {
671        RUSTERIX.write().unwrap().client.messages_font = ctx.ui.font.clone();
672
673        // Embedded Icons
674        for file in Embedded::iter() {
675            let name = file.as_ref();
676
677            if name.ends_with(".png") {
678                if let Some(file) = Embedded::get(name) {
679                    let data = std::io::Cursor::new(file.data);
680
681                    let decoder = png::Decoder::new(data);
682                    if let Ok(mut reader) = decoder.read_info() {
683                        if let Some(buffer_size) = reader.output_buffer_size() {
684                            let mut buf = vec![0; buffer_size];
685                            let info = reader.next_frame(&mut buf).unwrap();
686                            let bytes = &buf[..info.buffer_size()];
687
688                            let mut cut_name = name.replace("icons/", "");
689                            cut_name = cut_name.replace(".png", "");
690
691                            ctx.ui.add_icon(
692                                cut_name.to_string(),
693                                TheRGBABuffer::from(bytes.to_vec(), info.width, info.height),
694                            );
695                        }
696                    }
697                }
698            }
699        }
700
701        // ---
702
703        ui.set_statusbar_name("Statusbar".to_string());
704
705        let mut top_canvas = TheCanvas::new();
706        // Internal file/edit/game menu is hidden for the Xcode staticlib wrapper
707        // where native menu handling is expected.
708        #[cfg(not(feature = "staticlib"))]
709        {
710            let mut menu_canvas = TheCanvas::new();
711            let mut menu = TheMenu::new(TheId::named("Menu"));
712
713            let mut file_menu = TheContextMenu::named(fl!("menu_file"));
714            file_menu.add(TheContextMenuItem::new_with_accel(
715                fl!("menu_new"),
716                TheId::named("New"),
717                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'n'),
718            ));
719            file_menu.add_separator();
720            file_menu.add(TheContextMenuItem::new_with_accel(
721                fl!("menu_open"),
722                TheId::named("Open"),
723                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'o'),
724            ));
725            file_menu.add(TheContextMenuItem::new_with_accel(
726                fl!("menu_close"),
727                TheId::named("Close"),
728                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'w'),
729            ));
730            file_menu.add_separator();
731            file_menu.add(TheContextMenuItem::new_with_accel(
732                fl!("menu_save"),
733                TheId::named("Save"),
734                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 's'),
735            ));
736            file_menu.add(TheContextMenuItem::new_with_accel(
737                fl!("menu_save_as"),
738                TheId::named("Save As"),
739                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'a'),
740            ));
741            let mut edit_menu = TheContextMenu::named(fl!("menu_edit"));
742            edit_menu.add(TheContextMenuItem::new_with_accel(
743                fl!("menu_undo"),
744                TheId::named("Undo"),
745                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'z'),
746            ));
747            edit_menu.add(TheContextMenuItem::new_with_accel(
748                fl!("menu_redo"),
749                TheId::named("Redo"),
750                TheAccelerator::new(TheAcceleratorKey::CTRLCMD | TheAcceleratorKey::SHIFT, 'z'),
751            ));
752            edit_menu.add_separator();
753            edit_menu.add(TheContextMenuItem::new_with_accel(
754                fl!("menu_cut"),
755                TheId::named("Cut"),
756                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'x'),
757            ));
758            edit_menu.add(TheContextMenuItem::new_with_accel(
759                fl!("menu_copy"),
760                TheId::named("Copy"),
761                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'c'),
762            ));
763            edit_menu.add(TheContextMenuItem::new_with_accel(
764                fl!("menu_paste"),
765                TheId::named("Paste"),
766                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'v'),
767            ));
768            edit_menu.add_separator();
769            edit_menu.add(TheContextMenuItem::new_with_accel(
770                fl!("menu_apply_action"),
771                TheId::named("Action Apply"),
772                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'p'),
773            ));
774
775            let mut game_menu = TheContextMenu::named(fl!("game"));
776            game_menu.add(TheContextMenuItem::new_with_accel(
777                fl!("menu_play"),
778                TheId::named("Play"),
779                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'p'),
780            ));
781            game_menu.add(TheContextMenuItem::new_with_accel(
782                fl!("menu_pause"),
783                TheId::named("Pause"),
784                TheAccelerator::new(TheAcceleratorKey::CTRLCMD, 'o'),
785            ));
786            game_menu.add(TheContextMenuItem::new_with_accel(
787                fl!("menu_stop"),
788                TheId::named("Stop"),
789                TheAccelerator::new(TheAcceleratorKey::CTRLCMD | TheAcceleratorKey::SHIFT, 'p'),
790            ));
791
792            file_menu.register_accel(ctx);
793            edit_menu.register_accel(ctx);
794            game_menu.register_accel(ctx);
795
796            menu.add_context_menu(file_menu);
797            menu.add_context_menu(edit_menu);
798            menu.add_context_menu(game_menu);
799            menu_canvas.set_widget(menu);
800            top_canvas.set_top(menu_canvas);
801        }
802
803        let mut menubar = TheMenubar::new(TheId::named("Menubar"));
804        #[cfg(feature = "staticlib")]
805        menubar.limiter_mut().set_max_height(43);
806        #[cfg(not(feature = "staticlib"))]
807        menubar.limiter_mut().set_max_height(43 + 22);
808
809        let mut logo_button = TheMenubarButton::new(TheId::named("Logo"));
810        logo_button.set_icon_name("logo".to_string());
811        logo_button.set_status_text(&fl!("status_logo_button"));
812
813        let mut open_button = TheMenubarButton::new(TheId::named("Open"));
814        open_button.set_icon_name("icon_role_load".to_string());
815        open_button.set_status_text(&fl!("status_open_button"));
816
817        let mut save_button = TheMenubarButton::new(TheId::named("Save"));
818        save_button.set_status_text(&fl!("status_save_button"));
819        save_button.set_icon_name("icon_role_save".to_string());
820
821        let mut save_as_button = TheMenubarButton::new(TheId::named("Save As"));
822        save_as_button.set_icon_name("icon_role_save_as".to_string());
823        save_as_button.set_status_text(&fl!("status_save_as_button"));
824        save_as_button.set_icon_offset(Vec2::new(2, -5));
825
826        let mut undo_button = TheMenubarButton::new(TheId::named("Undo"));
827        undo_button.set_status_text(&fl!("status_undo_button"));
828        undo_button.set_icon_name("icon_role_undo".to_string());
829
830        let mut redo_button = TheMenubarButton::new(TheId::named("Redo"));
831        redo_button.set_status_text(&fl!("status_redo_button"));
832        redo_button.set_icon_name("icon_role_redo".to_string());
833
834        let mut play_button = TheMenubarButton::new(TheId::named("Play"));
835        play_button.set_status_text(&fl!("status_play_button"));
836        play_button.set_icon_name("play".to_string());
837        //play_button.set_fixed_size(vec2i(28, 28));
838
839        let mut pause_button = TheMenubarButton::new(TheId::named("Pause"));
840        pause_button.set_status_text(&fl!("status_pause_button"));
841        pause_button.set_icon_name("play-pause".to_string());
842
843        let mut stop_button = TheMenubarButton::new(TheId::named("Stop"));
844        stop_button.set_status_text(&fl!("status_stop_button"));
845        stop_button.set_icon_name("stop-fill".to_string());
846
847        let mut input_button = TheMenubarButton::new(TheId::named("GameInput"));
848        input_button.set_status_text(&fl!("status_game_input_button"));
849        input_button.set_icon_name("keyboard".to_string());
850        input_button.set_has_state(true);
851
852        let mut time_slider = TheTimeSlider::new(TheId::named("Server Time Slider"));
853        time_slider.set_status_text(&fl!("status_time_slider"));
854        time_slider.set_continuous(true);
855        time_slider.limiter_mut().set_max_width(400);
856        time_slider.set_value(TheValue::Time(TheTime::default()));
857
858        let mut patreon_button = TheMenubarButton::new(TheId::named("Patreon"));
859        patreon_button.set_status_text(&fl!("status_patreon_button"));
860        patreon_button.set_icon_name("patreon".to_string());
861        // patreon_button.set_fixed_size(vec2i(36, 36));
862        patreon_button.set_icon_offset(Vec2::new(-4, -2));
863
864        let mut help_button = TheMenubarButton::new(TheId::named("Help"));
865        help_button.set_status_text(&fl!("status_help_button"));
866        help_button.set_icon_name("question-mark".to_string());
867        help_button.set_has_state(true);
868        // patreon_button.set_fixed_size(vec2i(36, 36));
869        help_button.set_icon_offset(Vec2::new(-2, -2));
870
871        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
872        let mut update_button = {
873            let mut button = TheMenubarButton::new(TheId::named("Update"));
874            button.set_status_text(&fl!("status_update_button"));
875            button.set_icon_name("arrows-clockwise".to_string());
876            button
877        };
878
879        let mut hlayout = TheHLayout::new(TheId::named("Menu Layout"));
880        hlayout.set_background_color(None);
881        hlayout.set_margin(Vec4::new(10, 2, 10, 1));
882        hlayout.add_widget(Box::new(logo_button));
883        hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
884        hlayout.add_widget(Box::new(open_button));
885        hlayout.add_widget(Box::new(save_button));
886        hlayout.add_widget(Box::new(save_as_button));
887        hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
888        hlayout.add_widget(Box::new(undo_button));
889        hlayout.add_widget(Box::new(redo_button));
890        hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
891        hlayout.add_widget(Box::new(play_button));
892        hlayout.add_widget(Box::new(pause_button));
893        hlayout.add_widget(Box::new(stop_button));
894        hlayout.add_widget(Box::new(input_button));
895        hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
896        hlayout.add_widget(Box::new(time_slider));
897        //hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
898
899        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
900        {
901            hlayout.add_widget(Box::new(update_button));
902            hlayout.add_widget(Box::new(TheMenubarSeparator::new(TheId::empty())));
903            hlayout.add_widget(Box::new(patreon_button));
904            hlayout.set_reverse_index(Some(3));
905        }
906
907        #[cfg(not(all(not(target_arch = "wasm32"), feature = "self-update")))]
908        {
909            hlayout.add_widget(Box::new(patreon_button));
910            hlayout.add_widget(Box::new(help_button));
911            hlayout.set_reverse_index(Some(2));
912        }
913
914        top_canvas.set_widget(menubar);
915        top_canvas.set_layout(hlayout);
916        ui.canvas.set_top(top_canvas);
917
918        // Sidebar
919        self.sidebar.init_ui(ui, ctx, &mut self.server_ctx);
920
921        // Docks
922        let bottom_panels = DOCKMANAGER.write().unwrap().init(ctx);
923
924        let mut editor_canvas: TheCanvas = TheCanvas::new();
925
926        let mut editor_stack = TheStackLayout::new(TheId::named("Editor Stack"));
927        let poly_canvas = self.mapeditor.init_ui(ui, ctx, &mut self.project);
928        editor_stack.add_canvas(poly_canvas);
929
930        // Add Dock Editors
931        DOCKMANAGER
932            .write()
933            .unwrap()
934            .add_editors_to_stack(&mut editor_stack, ctx);
935
936        editor_canvas.set_layout(editor_stack);
937
938        // Main V Layout
939        let mut vsplitlayout = TheSharedVLayout::new(TheId::named("Shared VLayout"));
940        vsplitlayout.add_canvas(editor_canvas);
941        vsplitlayout.add_canvas(bottom_panels);
942        vsplitlayout.set_shared_ratio(crate::DEFAULT_VLAYOUT_RATIO);
943        vsplitlayout.set_mode(TheSharedVLayoutMode::Shared);
944
945        let mut shared_canvas = TheCanvas::new();
946        shared_canvas.set_layout(vsplitlayout);
947
948        let mut tabs_canvas = TheCanvas::new();
949        let mut tabs = TheTabbar::new(TheId::named("Project Tabs"));
950        tabs.limiter_mut().set_max_height(22);
951        tabs_canvas.set_widget(tabs);
952        shared_canvas.set_top(tabs_canvas);
953
954        // Tool List
955        let mut tool_list_canvas: TheCanvas = TheCanvas::new();
956
957        let mut tool_list_bar_canvas = TheCanvas::new();
958        tool_list_bar_canvas.set_widget(TheToolListBar::new(TheId::empty()));
959        tool_list_canvas.set_top(tool_list_bar_canvas);
960
961        let mut v_tool_list_layout = TheVLayout::new(TheId::named("Tool List Layout"));
962        v_tool_list_layout.limiter_mut().set_max_width(51);
963        v_tool_list_layout.set_margin(Vec4::new(2, 2, 2, 2));
964        v_tool_list_layout.set_padding(1);
965
966        TOOLLIST
967            .write()
968            .unwrap()
969            .set_active_editor(&mut v_tool_list_layout, ctx);
970
971        tool_list_canvas.set_layout(v_tool_list_layout);
972
973        let mut tool_list_border_canvas = TheCanvas::new();
974        let mut border_widget = TheIconView::new(TheId::empty());
975        border_widget.set_border_color(Some([82, 82, 82, 255]));
976        border_widget.limiter_mut().set_max_width(1);
977        border_widget.limiter_mut().set_max_height(i32::MAX);
978        tool_list_border_canvas.set_widget(border_widget);
979
980        tool_list_canvas.set_right(tool_list_border_canvas);
981        shared_canvas.set_left(tool_list_canvas);
982
983        ui.canvas.set_center(shared_canvas);
984
985        let mut status_canvas = TheCanvas::new();
986        let mut statusbar = TheStatusbar::new(TheId::named("Statusbar"));
987        statusbar.set_text(fl!("info_welcome"));
988        status_canvas.set_widget(statusbar);
989
990        ui.canvas.set_bottom(status_canvas);
991
992        // -
993
994        // ctx.ui.set_disabled("Save");
995        // ctx.ui.set_disabled("Save As");
996        ctx.ui.set_disabled("Undo");
997        ctx.ui.set_disabled("Redo");
998
999        // Init Rusterix
1000
1001        if let Some(icon) = ctx.ui.icon("light_on") {
1002            let texture = Texture::from_rgbabuffer(icon);
1003            self.build_values.set("light_on", Value::Texture(texture));
1004        }
1005        if let Some(icon) = ctx.ui.icon("light_off") {
1006            let texture = Texture::from_rgbabuffer(icon);
1007            self.build_values.set("light_off", Value::Texture(texture));
1008        }
1009        if let Some(icon) = ctx.ui.icon("character_on") {
1010            let texture = Texture::from_rgbabuffer(icon);
1011            self.build_values
1012                .set("character_on", Value::Texture(texture));
1013        }
1014        if let Some(icon) = ctx.ui.icon("character_off") {
1015            let texture = Texture::from_rgbabuffer(icon);
1016            self.build_values
1017                .set("character_off", Value::Texture(texture));
1018        }
1019        if let Some(icon) = ctx.ui.icon("treasure_on") {
1020            let texture = Texture::from_rgbabuffer(icon);
1021            self.build_values
1022                .set("treasure_on", Value::Texture(texture));
1023        }
1024        if let Some(icon) = ctx.ui.icon("treasure_off") {
1025            let texture = Texture::from_rgbabuffer(icon);
1026            self.build_values
1027                .set("treasure_off", Value::Texture(texture));
1028        }
1029
1030        RUSTERIX
1031            .write()
1032            .unwrap()
1033            .client
1034            .builder_d2
1035            .set_properties(&self.build_values);
1036        RUSTERIX.write().unwrap().set_d2();
1037        SCENEMANAGER
1038            .write()
1039            .unwrap()
1040            .set_apply_preview_filters(true);
1041        SCENEMANAGER.write().unwrap().startup();
1042
1043        self.event_receiver = Some(ui.add_state_listener("Main Receiver".into()));
1044        self.rebuild_project_tabs(ui);
1045    }
1046
1047    /// Set the command line arguments
1048    fn set_cmd_line_args(&mut self, args: Vec<String>, ctx: &mut TheContext) {
1049        if args.len() > 1 {
1050            let mut queued_any = false;
1051            for arg in args.iter().skip(1) {
1052                #[allow(irrefutable_let_patterns)]
1053                if let Ok(path) = PathBuf::from_str(arg) {
1054                    if !queued_any {
1055                        self.replace_next_project_load_in_active_tab = true;
1056                    }
1057                    ctx.ui.send(TheEvent::FileRequesterResult(
1058                        TheId::named("Open"),
1059                        vec![path],
1060                    ));
1061                    queued_any = true;
1062                }
1063            }
1064            if queued_any {
1065                return;
1066            }
1067        }
1068
1069        self.replace_next_project_load_in_active_tab = true;
1070        ctx.ui.send(TheEvent::StateChanged(
1071            TheId::named("New"),
1072            TheWidgetState::Clicked,
1073        ));
1074    }
1075
1076    /// Handle UI events and UI state
1077    fn update_ui(&mut self, ui: &mut TheUI, ctx: &mut TheContext) -> bool {
1078        let mut redraw = false;
1079        let mut update_server_icons = false;
1080
1081        // Make sure on first startup the active tool is properly selected
1082        if self.update_counter == 0 {
1083            let mut toollist = TOOLLIST.write().unwrap();
1084            let id = toollist.get_current_tool().id().uuid;
1085
1086            toollist.set_tool(id, ui, ctx, &mut self.project, &mut self.server_ctx);
1087        }
1088
1089        // Get build results from the scene manager if any
1090        while let Some(result) = SCENEMANAGER.write().unwrap().receive() {
1091            match result {
1092                SceneManagerResult::Startup => {
1093                    println!("Scene manager has started up.");
1094                }
1095                SceneManagerResult::ProcessedHeights(coord, heights) => {
1096                    if let Some(map) = &mut self.project.get_map_mut(&self.server_ctx) {
1097                        let local = map.terrain.get_chunk_coords(coord.x, coord.y);
1098                        if let Some(chunk) = &mut map.terrain.chunks.get_mut(&local) {
1099                            chunk.processed_heights = Some(heights);
1100                        }
1101                    }
1102                }
1103                SceneManagerResult::Chunk(chunk, togo, total, billboards) => {
1104                    if togo == 0 {
1105                        self.server_ctx.background_progress = None;
1106                    } else {
1107                        self.server_ctx.background_progress = Some(format!("{togo}/{total}"));
1108                    }
1109
1110                    let mut rusterix = RUSTERIX.write().unwrap();
1111
1112                    rusterix
1113                        .scene_handler
1114                        .vm
1115                        .execute(scenevm::Atom::RemoveChunkAt {
1116                            origin: chunk.origin,
1117                        });
1118
1119                    rusterix.scene_handler.vm.execute(scenevm::Atom::AddChunk {
1120                        id: Uuid::new_v4(),
1121                        chunk: chunk,
1122                    });
1123
1124                    // Add billboards to scene_handler (indexed by GeoId)
1125                    for billboard in billboards {
1126                        rusterix
1127                            .scene_handler
1128                            .billboards
1129                            .insert(billboard.geo_id, billboard);
1130                    }
1131
1132                    ctx.ui.send(TheEvent::Custom(
1133                        TheId::named("Update Minimap"),
1134                        TheValue::Empty,
1135                    ));
1136                }
1137                SceneManagerResult::UpdatedBatch3D(coord, batch) => {
1138                    let mut rusterix = RUSTERIX.write().unwrap();
1139                    if let Some(chunk) = rusterix.client.scene.chunks.get_mut(&coord) {
1140                        chunk.terrain_batch3d = Some(batch);
1141                    }
1142                }
1143                SceneManagerResult::Clear => {
1144                    let mut rusterix = RUSTERIX.write().unwrap();
1145                    rusterix
1146                        .scene_handler
1147                        .vm
1148                        .execute(scenevm::Atom::ClearGeometry);
1149
1150                    rusterix.scene_handler.billboards.clear();
1151                }
1152                SceneManagerResult::Quit => {
1153                    println!("Scene manager has shutdown.");
1154                }
1155            }
1156        }
1157
1158        // Check for redraw (30fps) and tick updates
1159        let redraw_ms = self.redraw_interval_ms();
1160        let tick_ms = CONFIGEDITOR.read().unwrap().game_tick_ms.max(1) as u64;
1161        let (mut redraw_update, tick_update) = self.update_tracker.update(redraw_ms, tick_ms);
1162
1163        // Handle queued UI events in the same update pass so input can trigger immediate redraw work.
1164        let mut pending_events = Vec::new();
1165        if let Some(receiver) = &mut self.event_receiver {
1166            while let Ok(event) = receiver.try_recv() {
1167                pending_events.push(event);
1168            }
1169        }
1170        if !pending_events.is_empty() {
1171            redraw_update = true;
1172        }
1173
1174        if tick_update {
1175            RUSTERIX.write().unwrap().client.inc_animation_frame();
1176
1177            self.server_ctx.animation_counter = self.server_ctx.animation_counter.wrapping_add(1);
1178            // To update animated minimaps (only for docks that need it)
1179            if DOCKMANAGER
1180                .read()
1181                .unwrap()
1182                .current_dock_supports_minimap_animation()
1183            {
1184                ctx.ui.send(TheEvent::Custom(
1185                    TheId::named("Soft Update Minimap"),
1186                    TheValue::Empty,
1187                ));
1188            }
1189        }
1190
1191        if redraw_update && !self.project.regions.is_empty() {
1192            // SCENEMANAGER.write().unwrap().tick();
1193            SCENEMANAGER.write().unwrap().tick_batch(8);
1194
1195            self.build_values.set(
1196                "no_rect_geo",
1197                Value::Bool(self.server_ctx.no_rect_geo_on_map),
1198            );
1199
1200            extract_build_values_from_config(&mut self.build_values);
1201
1202            let mut messages = Vec::new();
1203            let mut says = Vec::new();
1204            let mut choices = Vec::new();
1205
1206            // Update entities when the server is running
1207            {
1208                let rusterix = &mut RUSTERIX.write().unwrap();
1209                if rusterix.server.state == rusterix::ServerState::Running {
1210                    // Send a game tick to all servers
1211                    if tick_update {
1212                        rusterix.server.system_tick();
1213                    }
1214
1215                    // Send a redraw tick to all servers
1216                    if redraw_update {
1217                        rusterix.server.redraw_tick();
1218                    }
1219
1220                    if let Some(new_region_name) = rusterix.update_server() {
1221                        rusterix.client.current_map = new_region_name;
1222                    }
1223                    if rusterix.server.log_changed {
1224                        let log_text = rusterix.server.get_log();
1225                        ui.set_widget_value("LogEdit", ctx, TheValue::Text(log_text.clone()));
1226
1227                        // Auto-open Debug Log only when new log content contains warning/error.
1228                        let mut start = if log_text.len() < self.last_processed_log_len {
1229                            0
1230                        } else {
1231                            self.last_processed_log_len
1232                        };
1233                        while start < log_text.len() && !log_text.is_char_boundary(start) {
1234                            start += 1;
1235                        }
1236                        let new_segment = &log_text[start..];
1237                        if Self::log_segment_has_warning_or_error(new_segment) {
1238                            ctx.ui.send(TheEvent::StateChanged(
1239                                TheId::named("Debug Log"),
1240                                TheWidgetState::Clicked,
1241                            ));
1242                        }
1243                        self.last_processed_log_len = log_text.len();
1244                    }
1245                    for r in &mut self.project.regions {
1246                        rusterix.server.apply_entities_items(&mut r.map);
1247
1248                        if r.id == self.server_ctx.curr_region {
1249                            if let Some(time) = rusterix.server.get_time(&r.map.id) {
1250                                rusterix.client.set_server_time(time);
1251                                if let Some(widget) = ui.get_widget("Server Time Slider") {
1252                                    widget.set_value(TheValue::Time(rusterix.client.server_time));
1253                                }
1254                            }
1255
1256                            rusterix::tile_builder(&mut r.map, &mut rusterix.assets);
1257                            messages = rusterix.server.get_messages(&r.map.id);
1258                            says = rusterix.server.get_says(&r.map.id);
1259                            choices = rusterix.server.get_choices(&r.map.id);
1260                            for cmd in rusterix.server.get_audio_commands(&r.map.id) {
1261                                match cmd {
1262                                    AudioCommand::Play {
1263                                        name,
1264                                        bus,
1265                                        gain,
1266                                        looping,
1267                                    } => {
1268                                        rusterix.play_audio_on_bus(&name, &bus, gain, looping);
1269                                    }
1270                                    AudioCommand::ClearBus { bus } => {
1271                                        rusterix.clear_audio_bus(&bus);
1272                                    }
1273                                    AudioCommand::ClearAll => {
1274                                        rusterix.clear_all_audio();
1275                                    }
1276                                    AudioCommand::SetBusVolume { bus, volume } => {
1277                                        rusterix.set_audio_bus_volume(&bus, volume);
1278                                    }
1279                                }
1280                            }
1281
1282                            // Redraw the nodes
1283                            match &self.server_ctx.cc {
1284                                ContentContext::CharacterInstance(uuid) => {
1285                                    for entity in r.map.entities.iter() {
1286                                        if entity.creator_id == *uuid {
1287                                            CODEGRIDFX.write().unwrap().redraw_debug(
1288                                                ui,
1289                                                ctx,
1290                                                entity.id,
1291                                                &rusterix.server.debug,
1292                                            );
1293                                        }
1294                                    }
1295                                }
1296                                _ => {}
1297                            }
1298                        }
1299                    }
1300                }
1301            }
1302
1303            // Draw Map
1304            if let Some(render_view) = ui.get_render_view("PolyView") {
1305                let dim = *render_view.dim();
1306
1307                let buffer = render_view.render_buffer_mut();
1308                buffer.resize(dim.width, dim.height);
1309
1310                {
1311                    // If we are drawing billboard vertices in the geometry overlay, update them.
1312                    if !self.server_ctx.game_mode
1313                        && self.server_ctx.editor_view_mode != EditorViewMode::D2
1314                        && self.server_ctx.curr_map_tool_type == MapToolType::Vertex
1315                    {
1316                        TOOLLIST
1317                            .write()
1318                            .unwrap()
1319                            .update_geometry_overlay_3d(&mut self.project, &mut self.server_ctx);
1320                    }
1321
1322                    let rusterix = &mut RUSTERIX.write().unwrap();
1323                    let is_running = rusterix.server.state == rusterix::ServerState::Running;
1324
1325                    if is_running && self.server_ctx.game_mode {
1326                        for r in &mut self.project.regions {
1327                            if r.map.name == rusterix.client.current_map {
1328                                rusterix.draw_game(&r.map, messages, says, choices);
1329                                break;
1330                            }
1331                        }
1332
1333                        rusterix
1334                            .client
1335                            .insert_game_buffer(render_view.render_buffer_mut());
1336                    } else {
1337                        if self.server_ctx.editor_view_mode != EditorViewMode::D2
1338                            && self.server_ctx.get_map_context() == MapContext::Region
1339                        {
1340                            if let Some(region) =
1341                                self.project.get_region_ctx_mut(&mut self.server_ctx)
1342                            {
1343                                let follow_player_firstp = is_running
1344                                    && self.server_ctx.editor_view_mode == EditorViewMode::FirstP;
1345
1346                                if follow_player_firstp
1347                                    && let Some(player) =
1348                                        region.map.entities.iter().find(|e| e.is_player())
1349                                {
1350                                    let orientation =
1351                                        if player.orientation.magnitude_squared() > f32::EPSILON {
1352                                            player.orientation.normalized()
1353                                        } else {
1354                                            Vec2::new(1.0, 0.0)
1355                                        };
1356
1357                                    region.editing_position_3d = Vec3::new(
1358                                        player.position.x,
1359                                        player.position.y,
1360                                        player.position.z,
1361                                    );
1362                                    region.editing_look_at_3d = Vec3::new(
1363                                        player.position.x + orientation.x,
1364                                        player.position.y,
1365                                        player.position.z + orientation.y,
1366                                    );
1367                                } else {
1368                                    EDITCAMERA
1369                                        .write()
1370                                        .unwrap()
1371                                        .update_action(region, &mut self.server_ctx);
1372                                }
1373                                EDITCAMERA.write().unwrap().update_camera(
1374                                    region,
1375                                    &mut self.server_ctx,
1376                                    rusterix,
1377                                );
1378
1379                                // Keep editor 3D running mode in sync with runtime dynamic
1380                                // overlays (characters/items/lights).
1381                                let animation_frame = rusterix.client.animation_frame;
1382                                rusterix.build_dynamics_3d(&region.map, animation_frame);
1383                                rusterix.draw_d3(
1384                                    &region.map,
1385                                    render_view.render_buffer_mut().pixels_mut(),
1386                                    dim.width as usize,
1387                                    dim.height as usize,
1388                                );
1389                            }
1390                        } else
1391                        // Draw the region map
1392                        if self.server_ctx.get_map_context() == MapContext::Region
1393                            && self.server_ctx.editing_surface.is_none()
1394                        {
1395                            if let Some(region) =
1396                                self.project.get_region(&self.server_ctx.curr_region)
1397                            {
1398                                rusterix.client.builder_d2.set_clip_rect(None);
1399                                rusterix
1400                                    .client
1401                                    .builder_d2
1402                                    .set_map_tool_type(self.server_ctx.curr_map_tool_type);
1403                                if let Some(hover_cursor) = self.server_ctx.hover_cursor {
1404                                    rusterix.client.builder_d2.set_map_hover_info(
1405                                        self.server_ctx.hover,
1406                                        Some(vek::Vec2::new(hover_cursor.x, hover_cursor.y)),
1407                                    );
1408                                } else {
1409                                    rusterix
1410                                        .client
1411                                        .builder_d2
1412                                        .set_map_hover_info(self.server_ctx.hover, None);
1413                                }
1414
1415                                if let Some(camera_pos) = region.map.camera_xz {
1416                                    rusterix.client.builder_d2.set_camera_info(
1417                                        Some(Vec3::new(camera_pos.x, 0.0, camera_pos.y)),
1418                                        None,
1419                                    );
1420                                }
1421
1422                                // let start_time = ctx.get_time();
1423
1424                                if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1425                                    // During a paste operation we use a merged map
1426
1427                                    let mut map = region.map.clone();
1428                                    if let Some(hover) = self.server_ctx.hover_cursor {
1429                                        map.paste_at_position(clipboard, hover);
1430                                    }
1431
1432                                    rusterix.set_dirty();
1433                                    // rusterix.build_scene(
1434                                    //     Vec2::new(dim.width as f32, dim.height as f32),
1435                                    //     &map,
1436                                    //     &self.build_values,
1437                                    //     self.server_ctx.game_mode,
1438                                    // );
1439                                    rusterix.apply_entities_items(
1440                                        Vec2::new(dim.width as f32, dim.height as f32),
1441                                        &map,
1442                                        &self.server_ctx.editing_surface,
1443                                        false,
1444                                    );
1445                                } else {
1446                                    // rusterix.build_scene(
1447                                    //     Vec2::new(dim.width as f32, dim.height as f32),
1448                                    //     &region.map,
1449                                    //     &self.build_values,
1450                                    //     self.server_ctx.game_mode,
1451                                    // );
1452
1453                                    if let Some(map) = self.project.get_map(&self.server_ctx) {
1454                                        rusterix.apply_entities_items(
1455                                            Vec2::new(dim.width as f32, dim.height as f32),
1456                                            map,
1457                                            &self.server_ctx.editing_surface,
1458                                            false,
1459                                        );
1460                                    }
1461                                }
1462
1463                                // Prepare the messages for the region for drawing
1464                                rusterix.process_messages(&region.map, says);
1465
1466                                // let stop_time = ctx.get_time();
1467                                //println!("{} ms", stop_time - start_time);
1468                            }
1469
1470                            if let Some(map) = self.project.get_map_mut(&self.server_ctx) {
1471                                if self.server_ctx.editor_view_mode == EditorViewMode::D2 {
1472                                    rusterix.scene_handler.settings.backend_2d =
1473                                        RendererBackend::Raster;
1474                                    rusterix.set_d2();
1475                                }
1476                                if is_running
1477                                    && self.server_ctx.editor_view_mode == EditorViewMode::D2
1478                                {
1479                                    let animation_frame = rusterix.client.animation_frame;
1480                                    rusterix.build_dynamics_2d(map, animation_frame);
1481                                }
1482                                if self.server_ctx.editor_view_mode == EditorViewMode::D2
1483                                    && rusterix.scene_handler.vm.vm_layer_count() > 1
1484                                {
1485                                    rusterix.scene_handler.vm.set_layer_enabled(
1486                                        1,
1487                                        self.server_ctx.show_editing_geometry,
1488                                    );
1489                                }
1490                                rusterix.draw_scene(
1491                                    map,
1492                                    render_view.render_buffer_mut().pixels_mut(),
1493                                    dim.width as usize,
1494                                    dim.height as usize,
1495                                );
1496                            }
1497                        } else if self.server_ctx.get_map_context() == MapContext::Region
1498                            && self.server_ctx.editing_surface.is_some()
1499                        {
1500                            rusterix
1501                                .client
1502                                .builder_d2
1503                                .set_map_tool_type(self.server_ctx.curr_map_tool_type);
1504                            if let Some(profile) = self.project.get_map_mut(&self.server_ctx) {
1505                                if rusterix.scene_handler.vm.vm_layer_count() > 1 {
1506                                    // Profile editor relies on 2D overlay guides.
1507                                    rusterix.scene_handler.vm.set_layer_enabled(1, true);
1508                                }
1509                                if let Some(hover_cursor) = self.server_ctx.hover_cursor {
1510                                    rusterix.client.builder_d2.set_map_hover_info(
1511                                        self.server_ctx.hover,
1512                                        Some(vek::Vec2::new(hover_cursor.x, hover_cursor.y)),
1513                                    );
1514                                } else {
1515                                    rusterix
1516                                        .client
1517                                        .builder_d2
1518                                        .set_map_hover_info(self.server_ctx.hover, None);
1519                                }
1520
1521                                if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1522                                    // During a paste operation we use a merged map
1523                                    let mut map = profile.clone();
1524                                    if let Some(hover) = self.server_ctx.hover_cursor {
1525                                        map.paste_at_position(clipboard, hover);
1526                                    }
1527                                    rusterix.set_dirty();
1528                                    rusterix.build_custom_scene_d2(
1529                                        Vec2::new(dim.width as f32, dim.height as f32),
1530                                        &map,
1531                                        &self.build_values,
1532                                        &self.server_ctx.editing_surface,
1533                                        true,
1534                                    );
1535                                    rusterix.draw_custom_d2(
1536                                        &map,
1537                                        render_view.render_buffer_mut().pixels_mut(),
1538                                        dim.width as usize,
1539                                        dim.height as usize,
1540                                    );
1541                                } else {
1542                                    rusterix.build_custom_scene_d2(
1543                                        Vec2::new(dim.width as f32, dim.height as f32),
1544                                        profile,
1545                                        &self.build_values,
1546                                        &self.server_ctx.editing_surface,
1547                                        true,
1548                                    );
1549                                    rusterix.draw_custom_d2(
1550                                        profile,
1551                                        render_view.render_buffer_mut().pixels_mut(),
1552                                        dim.width as usize,
1553                                        dim.height as usize,
1554                                    );
1555                                }
1556                            }
1557                        } else
1558                        // Draw the screen / character / item map
1559                        if self.server_ctx.get_map_context() == MapContext::Character
1560                            || self.server_ctx.get_map_context() == MapContext::Item
1561                            || self.server_ctx.get_map_context() == MapContext::Screen
1562                        {
1563                            rusterix
1564                                .client
1565                                .builder_d2
1566                                .set_map_tool_type(self.server_ctx.curr_map_tool_type);
1567                            if let Some(map) = self.project.get_map_mut(&self.server_ctx) {
1568                                if rusterix.scene_handler.vm.vm_layer_count() > 1 {
1569                                    // Screen/character/item overlays should respect toggle.
1570                                    rusterix.scene_handler.vm.set_layer_enabled(
1571                                        1,
1572                                        self.server_ctx.show_editing_geometry,
1573                                    );
1574                                }
1575                                if let Some(hover_cursor) = self.server_ctx.hover_cursor {
1576                                    rusterix.client.builder_d2.set_map_hover_info(
1577                                        self.server_ctx.hover,
1578                                        Some(vek::Vec2::new(hover_cursor.x, hover_cursor.y)),
1579                                    );
1580                                } else {
1581                                    rusterix
1582                                        .client
1583                                        .builder_d2
1584                                        .set_map_hover_info(self.server_ctx.hover, None);
1585                                }
1586
1587                                if self.server_ctx.get_map_context() != MapContext::Screen {
1588                                    rusterix.client.builder_d2.set_clip_rect(Some(
1589                                        rusterix::Rect {
1590                                            x: -5.0,
1591                                            y: -5.0,
1592                                            width: 10.0,
1593                                            height: 10.0,
1594                                        },
1595                                    ));
1596                                } else {
1597                                    let viewport = CONFIGEDITOR.read().unwrap().viewport;
1598                                    let grid_size = CONFIGEDITOR.read().unwrap().grid_size as f32;
1599                                    let w = viewport.x as f32 / grid_size;
1600                                    let h = viewport.y as f32 / grid_size;
1601                                    rusterix.client.builder_d2.set_clip_rect(Some(
1602                                        rusterix::Rect {
1603                                            x: -w / 2.0,
1604                                            y: -h / 2.0,
1605                                            width: w,
1606                                            height: h,
1607                                        },
1608                                    ));
1609                                }
1610
1611                                if let Some(clipboard) = &self.server_ctx.paste_clipboard {
1612                                    // During a paste operation we use a merged map
1613                                    let mut map = map.clone();
1614                                    if let Some(hover) = self.server_ctx.hover_cursor {
1615                                        map.paste_at_position(clipboard, hover);
1616                                    }
1617                                    rusterix.set_dirty();
1618                                    rusterix.build_custom_scene_d2(
1619                                        Vec2::new(dim.width as f32, dim.height as f32),
1620                                        &map,
1621                                        &self.build_values,
1622                                        &self.server_ctx.editing_surface,
1623                                        true,
1624                                    );
1625                                    rusterix.draw_custom_d2(
1626                                        &map,
1627                                        render_view.render_buffer_mut().pixels_mut(),
1628                                        dim.width as usize,
1629                                        dim.height as usize,
1630                                    );
1631                                } else {
1632                                    rusterix.build_custom_scene_d2(
1633                                        Vec2::new(dim.width as f32, dim.height as f32),
1634                                        map,
1635                                        &self.build_values,
1636                                        &None,
1637                                        true,
1638                                    );
1639                                    rusterix.draw_custom_d2(
1640                                        map,
1641                                        render_view.render_buffer_mut().pixels_mut(),
1642                                        dim.width as usize,
1643                                        dim.height as usize,
1644                                    );
1645                                }
1646                            }
1647                        }
1648                    }
1649                }
1650                if !self.server_ctx.game_mode {
1651                    if let Some(map) = self.project.get_map_mut(&self.server_ctx) {
1652                        TOOLLIST.write().unwrap().draw_hud(
1653                            render_view.render_buffer_mut(),
1654                            map,
1655                            ctx,
1656                            &mut self.server_ctx,
1657                            &RUSTERIX.read().unwrap().assets,
1658                        );
1659                    }
1660                }
1661            }
1662
1663            // Draw the 3D Preview if active.
1664            // if !self.server_ctx.game_mode
1665            //     && self.server_ctx.curr_map_tool_helper == MapToolHelper::Preview
1666            // {
1667            //     if let Some(region) = self.project.get_region_ctx(&self.server_ctx) {
1668            //         PREVIEWVIEW
1669            //             .write()
1670            //             .unwrap()
1671            //             .draw(region, ui, ctx, &mut self.server_ctx);
1672            //     }
1673            // }
1674
1675            redraw = true;
1676        }
1677
1678        for event in pending_events {
1679            if self.server_ctx.help_mode
1680                && let Some(url) = self.help_url_for_editor_event(&event, ui)
1681            {
1682                ctx.ui.send(TheEvent::Custom(
1683                    TheId::named("Show Help"),
1684                    TheValue::Text(url),
1685                ));
1686                redraw = true;
1687                continue;
1688            }
1689
1690            if self.server_ctx.game_input_mode && !self.server_ctx.game_mode {
1691                // In game input mode send events to the game tool
1692                if let Some(game_tool) =
1693                    TOOLLIST.write().unwrap().get_game_tool_of_name("Game Tool")
1694                {
1695                    redraw = game_tool.handle_event(
1696                        &event,
1697                        ui,
1698                        ctx,
1699                        &mut self.project,
1700                        &mut self.server_ctx,
1701                    );
1702                }
1703            }
1704            if self
1705                .sidebar
1706                .handle_event(&event, ui, ctx, &mut self.project, &mut self.server_ctx)
1707            {
1708                redraw = true;
1709            }
1710            if TOOLLIST.write().unwrap().handle_event(
1711                &event,
1712                ui,
1713                ctx,
1714                &mut self.project,
1715                &mut self.server_ctx,
1716            ) {
1717                redraw = true;
1718            }
1719            if DOCKMANAGER.write().unwrap().handle_event(
1720                &event,
1721                ui,
1722                ctx,
1723                &mut self.project,
1724                &mut self.server_ctx,
1725            ) {
1726                redraw = true;
1727            }
1728            if self
1729                .mapeditor
1730                .handle_event(&event, ui, ctx, &mut self.project, &mut self.server_ctx)
1731            {
1732                redraw = true;
1733            }
1734            match event {
1735                TheEvent::IndexChanged(id, index) => {
1736                    if id.name == "Project Tabs" {
1737                        self.switch_to_session(
1738                            index,
1739                            ui,
1740                            ctx,
1741                            &mut update_server_icons,
1742                            &mut redraw,
1743                        );
1744                    }
1745                }
1746                TheEvent::CustomUndo(id, p, n) => {
1747                    if id.name == "ModuleUndo" {
1748                        let _ = (&p, &n);
1749                    }
1750                }
1751                TheEvent::Custom(id, value) => {
1752                    if id.name == "Show Help" {
1753                        if let TheValue::Text(url) = value {
1754                            _ = open::that(format!("https://www.eldiron.com/{}", url));
1755                            ctx.ui
1756                                .set_widget_state("Help".to_string(), TheWidgetState::None);
1757                            ctx.ui.clear_hover();
1758                            self.server_ctx.help_mode = false;
1759                            redraw = true;
1760                        }
1761                    }
1762                    if id.name == "Set Project Undo State" {
1763                        UNDOMANAGER.read().unwrap().set_undo_state_to_ui(ctx);
1764                    } else if id.name == "Render SceneManager Map" {
1765                        if self.server_ctx.pc.is_region() {
1766                            if self.server_ctx.editor_view_mode == EditorViewMode::D2
1767                                && self.server_ctx.profile_view.is_some()
1768                            {
1769                            } else {
1770                                crate::utils::scenemanager_render_map(
1771                                    &self.project,
1772                                    &self.server_ctx,
1773                                );
1774                                if self.server_ctx.editor_view_mode != EditorViewMode::D2 {
1775                                    TOOLLIST.write().unwrap().update_geometry_overlay_3d(
1776                                        &mut self.project,
1777                                        &mut self.server_ctx,
1778                                    );
1779                                }
1780                            }
1781                        }
1782                    } else if id.name == "Tool Changed" {
1783                        TOOLLIST
1784                            .write()
1785                            .unwrap()
1786                            .update_geometry_overlay_3d(&mut self.project, &mut self.server_ctx);
1787                    } else if id.name == "Update Client Properties" {
1788                        let mut rusterix = RUSTERIX.write().unwrap();
1789                        self.build_values.set(
1790                            "no_rect_geo",
1791                            rusterix::Value::Bool(self.server_ctx.no_rect_geo_on_map),
1792                        );
1793                        self.build_values.set(
1794                            "editing_slice",
1795                            rusterix::Value::Float(self.server_ctx.editing_slice),
1796                        );
1797                        self.build_values.set(
1798                            "editing_slice_height",
1799                            rusterix::Value::Float(self.server_ctx.editing_slice_height),
1800                        );
1801                        rusterix
1802                            .client
1803                            .builder_d2
1804                            .set_properties(&self.build_values);
1805                        rusterix.set_dirty();
1806                    }
1807                }
1808
1809                TheEvent::DialogValueOnClose(role, name, uuid, _value) => {
1810                    if name == "Delete Character Instance ?" {
1811                        if role == TheDialogButtonRole::Delete {
1812                            if let Some(region) =
1813                                self.project.get_region_mut(&self.server_ctx.curr_region)
1814                            {
1815                                let character_id = uuid;
1816                                if region.characters.shift_remove(&character_id).is_some() {
1817                                    self.server_ctx.curr_region_content = ContentContext::Unknown;
1818                                    region.map.selected_entity_item = None;
1819                                    redraw = true;
1820
1821                                    // Remove from the content list
1822                                    if let Some(list) = ui.get_list_layout("Region Content List") {
1823                                        list.remove(TheId::named_with_id(
1824                                            "Region Content List Item",
1825                                            character_id,
1826                                        ));
1827                                        ui.select_first_list_item("Region Content List", ctx);
1828                                        ctx.ui.relayout = true;
1829                                    }
1830                                    insert_content_into_maps(&mut self.project);
1831                                    RUSTERIX.write().unwrap().set_dirty();
1832                                }
1833                            }
1834                        }
1835                    } else if name == "Delete Item Instance ?" {
1836                        if role == TheDialogButtonRole::Delete {
1837                            if let Some(region) =
1838                                self.project.get_region_mut(&self.server_ctx.curr_region)
1839                            {
1840                                let item_id = uuid;
1841                                if region.items.shift_remove(&item_id).is_some() {
1842                                    self.server_ctx.curr_region_content = ContentContext::Unknown;
1843                                    redraw = true;
1844
1845                                    // Remove from the content list
1846                                    if let Some(list) = ui.get_list_layout("Region Content List") {
1847                                        list.remove(TheId::named_with_id(
1848                                            "Region Content List Item",
1849                                            item_id,
1850                                        ));
1851                                        ui.select_first_list_item("Region Content List", ctx);
1852                                        ctx.ui.relayout = true;
1853                                    }
1854                                    insert_content_into_maps(&mut self.project);
1855                                    RUSTERIX.write().unwrap().set_dirty();
1856                                }
1857                            }
1858                        }
1859                    } else if name == "Close Project Tab" && role == TheDialogButtonRole::Accept {
1860                        self.close_active_session(ui, ctx, &mut update_server_icons, &mut redraw);
1861                    } else if name == "Update Eldiron" && role == TheDialogButtonRole::Accept {
1862                        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
1863                        {
1864                            let updater = self.self_updater.lock().unwrap();
1865
1866                            if updater.has_newer_release() {
1867                                let release = updater.latest_release().cloned().unwrap();
1868
1869                                let updater = Arc::clone(&self.self_updater);
1870                                let tx = self.self_update_tx.clone();
1871
1872                                self.self_update_tx
1873                                    .send(SelfUpdateEvent::UpdateStart(release.clone()))
1874                                    .unwrap();
1875
1876                                thread::spawn(move || {
1877                                    match updater.lock().unwrap().update_latest() {
1878                                        Ok(status) => match status {
1879                                            self_update::Status::UpToDate(_) => {
1880                                                tx.send(SelfUpdateEvent::AlreadyUpToDate).unwrap();
1881                                            }
1882                                            self_update::Status::Updated(_) => {
1883                                                tx.send(SelfUpdateEvent::UpdateCompleted(release))
1884                                                    .unwrap();
1885                                            }
1886                                        },
1887                                        Err(err) => {
1888                                            tx.send(SelfUpdateEvent::UpdateError(err.to_string()))
1889                                                .unwrap();
1890                                        }
1891                                    }
1892                                });
1893                            } else {
1894                                self.self_update_tx
1895                                    .send(SelfUpdateEvent::AlreadyUpToDate)
1896                                    .unwrap();
1897                            }
1898                        }
1899                    }
1900                }
1901                TheEvent::RenderViewDrop(_id, location, drop) => {
1902                    if drop.id.name.starts_with("Shader") {
1903                        return true;
1904                    }
1905
1906                    let mut grid_pos = Vec2::zero();
1907                    let mut spawn_y = 0.0;
1908
1909                    if let Some(map) = self.project.get_map(&self.server_ctx) {
1910                        if let Some(render_view) = ui.get_render_view("PolyView") {
1911                            let dim = *render_view.dim();
1912                            grid_pos = self.server_ctx.local_to_map_cell(
1913                                Vec2::new(dim.width as f32, dim.height as f32),
1914                                Vec2::new(location.x as f32, location.y as f32),
1915                                map,
1916                                map.subdivisions,
1917                            );
1918                            grid_pos += 0.5;
1919                            let mut best_height: Option<f32> = None;
1920                            for sector in map
1921                                .sectors
1922                                .iter()
1923                                .filter(|s| s.layer.is_none() && s.is_inside(map, grid_pos))
1924                            {
1925                                let mut vertex_ids: Vec<u32> = Vec::new();
1926                                let mut sum_y = 0.0f32;
1927                                let mut count = 0usize;
1928                                for linedef_id in &sector.linedefs {
1929                                    if let Some(ld) = map.find_linedef(*linedef_id) {
1930                                        if !vertex_ids.contains(&ld.start_vertex) {
1931                                            vertex_ids.push(ld.start_vertex);
1932                                            if let Some(v) = map.get_vertex_3d(ld.start_vertex) {
1933                                                sum_y += v.y;
1934                                                count += 1;
1935                                            }
1936                                        }
1937                                        if !vertex_ids.contains(&ld.end_vertex) {
1938                                            vertex_ids.push(ld.end_vertex);
1939                                            if let Some(v) = map.get_vertex_3d(ld.end_vertex) {
1940                                                sum_y += v.y;
1941                                                count += 1;
1942                                            }
1943                                        }
1944                                    }
1945                                }
1946                                if count > 0 {
1947                                    let h = sum_y / count as f32;
1948                                    best_height = Some(best_height.map_or(h, |prev| prev.max(h)));
1949                                }
1950                            }
1951                            if let Some(h) = best_height {
1952                                spawn_y = h;
1953                            }
1954                        }
1955                    }
1956
1957                    if drop.id.name.starts_with("Character") {
1958                        let mut instance = Character {
1959                            character_id: drop.id.references,
1960                            position: Vec3::new(grid_pos.x, spawn_y, grid_pos.y),
1961                            ..Default::default()
1962                        };
1963
1964                        if let Some(bytes) = crate::Embedded::get("python/instcharacter.py") {
1965                            if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
1966                                instance.source = source.to_string();
1967                            }
1968                        }
1969
1970                        let mut name = "Character".to_string();
1971                        if let Some(character) = self.project.characters.get(&drop.id.references) {
1972                            name.clone_from(&character.name);
1973                        }
1974                        instance.name = name.clone();
1975
1976                        let atom = ProjectUndoAtom::AddRegionCharacterInstance(
1977                            self.server_ctx.curr_region,
1978                            instance,
1979                        );
1980                        atom.redo(&mut self.project, ui, ctx, &mut self.server_ctx);
1981                        UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
1982                    } else if drop.id.name.starts_with("Item") {
1983                        let mut instance = Item {
1984                            item_id: drop.id.references,
1985                            position: Vec3::new(grid_pos.x, spawn_y, grid_pos.y),
1986                            ..Default::default()
1987                        };
1988
1989                        if let Some(bytes) = crate::Embedded::get("python/institem.py") {
1990                            if let Ok(source) = std::str::from_utf8(bytes.data.as_ref()) {
1991                                instance.source = source.to_string();
1992                            }
1993                        }
1994
1995                        let mut name = "Item".to_string();
1996                        if let Some(item) = self.project.items.get(&drop.id.references) {
1997                            name.clone_from(&item.name);
1998                        }
1999                        instance.name = name;
2000
2001                        let atom = ProjectUndoAtom::AddRegionItemInstance(
2002                            self.server_ctx.curr_region,
2003                            instance,
2004                        );
2005                        atom.redo(&mut self.project, ui, ctx, &mut self.server_ctx);
2006                        UNDOMANAGER.write().unwrap().add_undo(atom, ctx);
2007                    }
2008                }
2009                /*
2010                TheEvent::TileEditorDrop(_id, location, drop) => {
2011                    if drop.id.name.starts_with("Character") {
2012                        let mut instance = TheCodeBundle::new();
2013
2014                        let mut init = TheCodeGrid {
2015                            name: "init".into(),
2016                            ..Default::default()
2017                        };
2018                        init.insert_atom(
2019                            (0, 0),
2020                            TheCodeAtom::Set(
2021                                "@self.position".to_string(),
2022                                TheValueAssignment::Assign,
2023                            ),
2024                        );
2025                        init.insert_atom(
2026                            (1, 0),
2027                            TheCodeAtom::Assignment(TheValueAssignment::Assign),
2028                        );
2029                        init.insert_atom(
2030                            (2, 0),
2031                            TheCodeAtom::Value(TheValue::Position(Vec3::new(
2032                                location.x as f32,
2033                                0.0,
2034                                location.y as f32,
2035                            ))),
2036                        );
2037                        instance.insert_grid(init);
2038
2039                        // Set the character instance bundle, disabled for now
2040
2041                        // self.sidebar.code_editor.set_bundle(
2042                        //     instance.clone(),
2043                        //     ctx,
2044                        //     self.sidebar.width,
2045                        // );
2046
2047                        let character = Character {
2048                            id: instance.id,
2049                            character_id: drop.id.uuid,
2050                            instance,
2051                        };
2052
2053                        // Add the character instance to the region content list
2054
2055                        let mut name = "Character".to_string();
2056                        if let Some(character) = self.project.characters.get(&drop.id.uuid) {
2057                            name.clone_from(&character.name);
2058                        }
2059
2060                        if let Some(list) = ui.get_list_layout("Region Content List") {
2061                            let mut item = TheListItem::new(TheId::named_with_id(
2062                                "Region Content List Item",
2063                                character.id,
2064                            ));
2065                            item.set_text(name);
2066                            item.set_state(TheWidgetState::Selected);
2067                            item.add_value_column(100, TheValue::Text("Character".to_string()));
2068
2069                            list.deselect_all();
2070                            item.set_context_menu(Some(TheContextMenu {
2071                                items: vec![TheContextMenuItem::new(
2072                                    "Delete Character...".to_string(),
2073                                    TheId::named("Sidebar Delete Character Instance"),
2074                                )],
2075                                ..Default::default()
2076                            }));
2077                            list.add_item(item, ctx);
2078                            list.select_item(character.id, ctx, true);
2079                        }
2080
2081                        // Add the character instance to the project
2082
2083                        if let Some(region) =
2084                            self.project.get_region_mut(&self.server_ctx.curr_region)
2085                        {
2086                            region.characters.insert(character.id, character.clone());
2087                        }
2088
2089                        // Add the character instance to the server
2090
2091                        self.server_ctx.curr_character = Some(character.character_id);
2092                        self.server_ctx.curr_character_instance = Some(character.id);
2093                        self.server_ctx.curr_area = None;
2094                        //self.sidebar.deselect_all("Character List", ui);
2095
2096                        self.server_ctx.curr_grid_id =
2097                            self.server.add_character_instance_to_region(
2098                                self.server_ctx.curr_region,
2099                                character,
2100                                None,
2101                            );
2102
2103                        // Set the character instance debug info, disabled for now
2104
2105                        // if let Some(curr_grid_id) = self.server_ctx.curr_grid_id {
2106                        //     let debug_module = self.server.get_region_debug_module(
2107                        //         self.server_ctx.curr_region,
2108                        //         curr_grid_id,
2109                        //     );
2110
2111                        //     self.sidebar.code_editor.set_debug_module(debug_module, ui);
2112                        // }
2113                    } else if drop.id.name.starts_with("Item") {
2114                        let mut instance = TheCodeBundle::new();
2115
2116                        let mut init = TheCodeGrid {
2117                            name: "init".into(),
2118                            ..Default::default()
2119                        };
2120                        init.insert_atom(
2121                            (0, 0),
2122                            TheCodeAtom::Set(
2123                                "@self.position".to_string(),
2124                                TheValueAssignment::Assign,
2125                            ),
2126                        );
2127                        init.insert_atom(
2128                            (1, 0),
2129                            TheCodeAtom::Assignment(TheValueAssignment::Assign),
2130                        );
2131                        init.insert_atom(
2132                            (2, 0),
2133                            TheCodeAtom::Value(TheValue::Position(Vec3::new(
2134                                location.x as f32,
2135                                0.0,
2136                                location.y as f32,
2137                            ))),
2138                        );
2139                        instance.insert_grid(init);
2140
2141                        // Set the character instance bundle, disabled for now
2142
2143                        // self.sidebar.code_editor.set_bundle(
2144                        //     instance.clone(),
2145                        //     ctx,
2146                        //     self.sidebar.width,
2147                        // );
2148
2149                        let item = Item {
2150                            id: instance.id,
2151                            item_id: drop.id.uuid,
2152                            instance,
2153                        };
2154
2155                        // Add the item instance to the region content list
2156
2157                        let mut name = "Item".to_string();
2158                        if let Some(item) = self.project.items.get(&drop.id.uuid) {
2159                            name.clone_from(&item.name);
2160                        }
2161
2162                        if let Some(list) = ui.get_list_layout("Region Content List") {
2163                            let mut list_item = TheListItem::new(TheId::named_with_id(
2164                                "Region Content List Item",
2165                                item.id,
2166                            ));
2167                            list_item.set_text(name);
2168                            list_item.set_state(TheWidgetState::Selected);
2169                            list_item.add_value_column(100, TheValue::Text("Item".to_string()));
2170
2171                            list.deselect_all();
2172                            list.add_item(list_item, ctx);
2173                            list.select_item(item.id, ctx, true);
2174                        }
2175
2176                        // Add the item instance to the project
2177
2178                        if let Some(region) =
2179                            self.project.get_region_mut(&self.server_ctx.curr_region)
2180                        {
2181                            region.items.insert(item.id, item.clone());
2182                        }
2183
2184                        // Add the character instance to the server
2185
2186                        self.server_ctx.curr_character = None;
2187                        self.server_ctx.curr_character_instance = None;
2188                        self.server_ctx.curr_item = Some(item.item_id);
2189                        self.server_ctx.curr_item_instance = Some(item.id);
2190                        self.server_ctx.curr_area = None;
2191
2192                        self.server_ctx.curr_grid_id = self
2193                            .server
2194                            .add_item_instance_to_region(self.server_ctx.curr_region, item);
2195
2196                        // Set the character instance debug info, disabled for now
2197
2198                        // if let Some(curr_grid_id) = self.server_ctx.curr_grid_id {
2199                        //     let debug_module = self.server.get_region_debug_module(
2200                        //         self.server_ctx.curr_region,
2201                        //         curr_grid_id,
2202                        //     );
2203
2204                        //     self.sidebar.code_editor.set_debug_module(debug_module, ui);
2205                        // }
2206                    }
2207                }*/
2208                TheEvent::FileRequesterResult(id, paths) => {
2209                    // Load a palette from a file
2210                    if id.name == "Palette Import" {
2211                        for p in paths {
2212                            let contents = std::fs::read_to_string(p).unwrap_or("".to_string());
2213                            let prev = self.project.palette.clone();
2214                            self.project.palette.load_from_txt(contents);
2215                            *PALETTE.write().unwrap() = self.project.palette.clone();
2216
2217                            if let Some(palette_picker) = ui.get_palette_picker("Palette Picker") {
2218                                let index = palette_picker.index();
2219
2220                                palette_picker.set_palette(self.project.palette.clone());
2221                                if let Some(widget) = ui.get_widget("Palette Color Picker") {
2222                                    if let Some(color) = &self.project.palette[index] {
2223                                        widget.set_value(TheValue::ColorObject(color.clone()));
2224                                    }
2225                                }
2226                                if let Some(widget) = ui.get_widget("Palette Hex Edit") {
2227                                    if let Some(color) = &self.project.palette[index] {
2228                                        widget.set_value(TheValue::Text(color.to_hex()));
2229                                    }
2230                                }
2231                            }
2232                            redraw = true;
2233
2234                            let undo =
2235                                ProjectUndoAtom::PaletteEdit(prev, self.project.palette.clone());
2236                            UNDOMANAGER.write().unwrap().add_undo(undo, ctx);
2237                        }
2238                    } else
2239                    // Open
2240                    if id.name == "Open" {
2241                        for p in paths {
2242                            if let Ok(contents) = std::fs::read_to_string(&p) {
2243                                if let Ok(mut loaded) = serde_json::from_str::<Project>(&contents) {
2244                                    loaded.palette.current_index = 0;
2245                                    Self::sanitize_loaded_project(&mut loaded);
2246
2247                                    self.sync_active_session_from_editor();
2248                                    let new_index = if self.replace_next_project_load_in_active_tab
2249                                    {
2250                                        self.sessions[self.active_session] = ProjectSession {
2251                                            project: loaded,
2252                                            project_path: Some(p.clone()),
2253                                            undo: UndoManager::default(),
2254                                            dirty: false,
2255                                        };
2256                                        self.replace_next_project_load_in_active_tab = false;
2257                                        self.active_session
2258                                    } else {
2259                                        self.sessions.push(ProjectSession {
2260                                            project: loaded,
2261                                            project_path: Some(p.clone()),
2262                                            undo: UndoManager::default(),
2263                                            dirty: false,
2264                                        });
2265                                        self.sessions.len() - 1
2266                                    };
2267                                    self.switch_to_session(
2268                                        new_index,
2269                                        ui,
2270                                        ctx,
2271                                        &mut update_server_icons,
2272                                        &mut redraw,
2273                                    );
2274                                    ctx.ui.send(TheEvent::SetStatusText(
2275                                        TheId::empty(),
2276                                        "Project loaded successfully.".to_string(),
2277                                    ));
2278                                } else {
2279                                    self.replace_next_project_load_in_active_tab = false;
2280                                    ctx.ui.send(TheEvent::SetStatusText(
2281                                        TheId::empty(),
2282                                        "Unable to load project!".to_string(),
2283                                    ));
2284                                }
2285                            }
2286                        }
2287                    } else if id.name == "Save As" {
2288                        for p in paths {
2289                            self.persist_active_region_view_state();
2290                            let json = serde_json::to_string(&self.project);
2291                            if let Ok(json) = json {
2292                                if std::fs::write(p.clone(), json).is_ok() {
2293                                    self.project_path = Some(p);
2294                                    UNDOMANAGER.write().unwrap().mark_saved();
2295                                    DOCKMANAGER.write().unwrap().mark_saved();
2296                                    if self.active_session < self.sessions.len() {
2297                                        self.sessions[self.active_session].dirty = false;
2298                                    }
2299                                    self.sync_active_session_from_editor();
2300                                    self.rebuild_project_tabs(ui);
2301                                    ctx.ui.send(TheEvent::SetStatusText(
2302                                        TheId::empty(),
2303                                        "Project saved successfully.".to_string(),
2304                                    ))
2305                                } else {
2306                                    ctx.ui.send(TheEvent::SetStatusText(
2307                                        TheId::empty(),
2308                                        "Unable to save project!".to_string(),
2309                                    ))
2310                                }
2311                            }
2312                        }
2313                    }
2314                }
2315                TheEvent::StateChanged(id, state) => {
2316                    if id.name == "Help" {
2317                        self.server_ctx.help_mode = state == TheWidgetState::Clicked;
2318                    }
2319                    if id.name == "GameInput" {
2320                        self.server_ctx.game_input_mode = state == TheWidgetState::Clicked;
2321                    } else if id.name == "New" {
2322                        let mut project = Project::default();
2323                        if let Some(bytes) = crate::Embedded::get("starter_project.eldiron")
2324                            && let Ok(project_string) = std::str::from_utf8(bytes.data.as_ref())
2325                            && let Ok(loaded) = serde_json::from_str(&project_string.to_string())
2326                        {
2327                            project = loaded;
2328                        }
2329                        Self::sanitize_loaded_project(&mut project);
2330
2331                        self.sync_active_session_from_editor();
2332                        let new_index = if self.replace_next_project_load_in_active_tab {
2333                            self.sessions[self.active_session] = ProjectSession {
2334                                project,
2335                                project_path: None,
2336                                undo: UndoManager::default(),
2337                                dirty: false,
2338                            };
2339                            self.replace_next_project_load_in_active_tab = false;
2340                            self.active_session
2341                        } else {
2342                            self.sessions.push(ProjectSession {
2343                                project,
2344                                project_path: None,
2345                                undo: UndoManager::default(),
2346                                dirty: false,
2347                            });
2348                            self.sessions.len() - 1
2349                        };
2350                        self.switch_to_session(
2351                            new_index,
2352                            ui,
2353                            ctx,
2354                            &mut update_server_icons,
2355                            &mut redraw,
2356                        );
2357
2358                        ctx.ui.send(TheEvent::SetStatusText(
2359                            TheId::empty(),
2360                            "New project successfully initialized.".to_string(),
2361                        ));
2362                    } else if id.name == "Logo" {
2363                        _ = open::that("https://eldiron.com");
2364                        ctx.ui
2365                            .set_widget_state("Logo".to_string(), TheWidgetState::None);
2366                        ctx.ui.clear_hover();
2367                        redraw = true;
2368                    } else if id.name == "Patreon" {
2369                        _ = open::that("https://www.patreon.com/eldiron");
2370                        ctx.ui
2371                            .set_widget_state("Patreon".to_string(), TheWidgetState::None);
2372                        ctx.ui.clear_hover();
2373                        redraw = true;
2374                    } else if id.name == "Update" {
2375                        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
2376                        {
2377                            let updater = self.self_updater.lock().unwrap();
2378
2379                            if updater.has_newer_release() {
2380                                self.self_update_tx
2381                                    .send(SelfUpdateEvent::UpdateConfirm(
2382                                        updater.latest_release().cloned().unwrap(),
2383                                    ))
2384                                    .unwrap();
2385                            } else {
2386                                if let Some(statusbar) = ui.get_widget("Statusbar") {
2387                                    statusbar
2388                                        .as_statusbar()
2389                                        .unwrap()
2390                                        .set_text(fl!("info_update_check"));
2391                                }
2392
2393                                let updater = Arc::clone(&self.self_updater);
2394                                let tx = self.self_update_tx.clone();
2395
2396                                thread::spawn(move || {
2397                                    let mut updater = updater.lock().unwrap();
2398
2399                                    match updater.fetch_release_list() {
2400                                        Ok(_) => {
2401                                            if updater.has_newer_release() {
2402                                                tx.send(SelfUpdateEvent::UpdateConfirm(
2403                                                    updater.latest_release().cloned().unwrap(),
2404                                                ))
2405                                                .unwrap();
2406                                            } else {
2407                                                tx.send(SelfUpdateEvent::AlreadyUpToDate).unwrap();
2408                                            }
2409                                        }
2410                                        Err(err) => {
2411                                            tx.send(SelfUpdateEvent::UpdateError(err.to_string()))
2412                                                .unwrap();
2413                                        }
2414                                    }
2415                                });
2416                            }
2417
2418                            ctx.ui
2419                                .set_widget_state("Update".to_string(), TheWidgetState::None);
2420                            ctx.ui.clear_hover();
2421                            redraw = true;
2422                        }
2423                    } else if id.name == "Open" {
2424                        ctx.ui.open_file_requester(
2425                            TheId::named_with_id(id.name.as_str(), Uuid::new_v4()),
2426                            "Open".into(),
2427                            TheFileExtension::new("Eldiron".into(), vec!["eldiron".to_string()]),
2428                        );
2429                        ctx.ui
2430                            .set_widget_state("Open".to_string(), TheWidgetState::None);
2431                        ctx.ui.clear_hover();
2432                        redraw = true;
2433                    } else if id.name == "Close" {
2434                        if self.active_session_has_changes() {
2435                            let uuid = Uuid::new_v4();
2436                            let width = 380;
2437                            let height = 110;
2438
2439                            let mut canvas = TheCanvas::new();
2440                            canvas.limiter_mut().set_max_size(Vec2::new(width, height));
2441
2442                            let mut hlayout: TheHLayout = TheHLayout::new(TheId::empty());
2443                            hlayout.limiter_mut().set_max_width(width);
2444
2445                            let mut text_widget =
2446                                TheText::new(TheId::named_with_id("Dialog Value", uuid));
2447                            text_widget.set_text(
2448                                "This tab has unsaved changes. Close it anyway?".to_string(),
2449                            );
2450                            text_widget.limiter_mut().set_max_width(280);
2451                            hlayout.add_widget(Box::new(text_widget));
2452
2453                            canvas.set_layout(hlayout);
2454                            ui.show_dialog(
2455                                "Close Project Tab",
2456                                canvas,
2457                                vec![TheDialogButtonRole::Accept, TheDialogButtonRole::Reject],
2458                                ctx,
2459                            );
2460                        } else {
2461                            self.close_active_session(
2462                                ui,
2463                                ctx,
2464                                &mut update_server_icons,
2465                                &mut redraw,
2466                            );
2467                        }
2468                        ctx.ui
2469                            .set_widget_state("Close".to_string(), TheWidgetState::None);
2470                        ctx.ui.clear_hover();
2471                        redraw = true;
2472                    } else if id.name == "Save" {
2473                        if let Some(path) = self.project_path.clone() {
2474                            let mut success = false;
2475                            // if let Ok(output) = postcard::to_allocvec(&self.project) {
2476                            self.persist_active_region_view_state();
2477                            if let Ok(output) = serde_json::to_string(&self.project) {
2478                                if std::fs::write(&path, output).is_ok() {
2479                                    UNDOMANAGER.write().unwrap().mark_saved();
2480                                    DOCKMANAGER.write().unwrap().mark_saved();
2481                                    if self.active_session < self.sessions.len() {
2482                                        self.sessions[self.active_session].dirty = false;
2483                                    }
2484                                    self.sync_active_session_from_editor();
2485                                    self.rebuild_project_tabs(ui);
2486                                    ctx.ui.send(TheEvent::SetStatusText(
2487                                        TheId::empty(),
2488                                        "Project saved successfully.".to_string(),
2489                                    ));
2490                                    success = true;
2491                                }
2492                            }
2493
2494                            if !success {
2495                                ctx.ui.send(TheEvent::SetStatusText(
2496                                    TheId::empty(),
2497                                    "Unable to save project!".to_string(),
2498                                ))
2499                            }
2500                        } else {
2501                            ctx.ui.send(TheEvent::StateChanged(
2502                                TheId::named("Save As"),
2503                                TheWidgetState::Clicked,
2504                            ));
2505                            ctx.ui
2506                                .set_widget_state("Save".to_string(), TheWidgetState::None);
2507                        }
2508                    } else if id.name == "Save As" {
2509                        ctx.ui.save_file_requester(
2510                            TheId::named_with_id(id.name.as_str(), Uuid::new_v4()),
2511                            "Save".into(),
2512                            TheFileExtension::new("Eldiron".into(), vec!["eldiron".to_string()]),
2513                        );
2514                        ctx.ui
2515                            .set_widget_state("Save As".to_string(), TheWidgetState::None);
2516                        ctx.ui.clear_hover();
2517                        redraw = true;
2518                    }
2519                    // Server
2520                    else if id.name == "Play" {
2521                        let state = RUSTERIX.read().unwrap().server.state;
2522                        if state == rusterix::ServerState::Paused {
2523                            RUSTERIX.write().unwrap().server.continue_instances();
2524                            update_server_icons = true;
2525                        } else {
2526                            if state == rusterix::ServerState::Off {
2527                                start_server(
2528                                    &mut RUSTERIX.write().unwrap(),
2529                                    &mut self.project,
2530                                    false,
2531                                );
2532                                RUSTERIX.write().unwrap().clear_say_messages();
2533                                let commands =
2534                                    setup_client(&mut RUSTERIX.write().unwrap(), &mut self.project);
2535                                RUSTERIX
2536                                    .write()
2537                                    .unwrap()
2538                                    .server
2539                                    .process_client_commands(commands);
2540                                ctx.ui.send(TheEvent::SetStatusText(
2541                                    TheId::empty(),
2542                                    "Server has been started.".to_string(),
2543                                ));
2544                                ui.set_widget_value("LogEdit", ctx, TheValue::Text(String::new()));
2545                                self.last_processed_log_len = 0;
2546                                RUSTERIX.write().unwrap().player_camera = PlayerCamera::D2;
2547                            }
2548                            update_server_icons = true;
2549                        }
2550                    } else if id.name == "Pause" {
2551                        let state = RUSTERIX.read().unwrap().server.state;
2552                        if state == rusterix::ServerState::Running {
2553                            RUSTERIX.write().unwrap().server.pause();
2554                            update_server_icons = true;
2555                        }
2556                    } else if id.name == "Stop" {
2557                        RUSTERIX.write().unwrap().server.stop();
2558                        RUSTERIX.write().unwrap().clear_say_messages();
2559                        RUSTERIX.write().unwrap().player_camera = PlayerCamera::D2;
2560
2561                        ui.set_widget_value("InfoView", ctx, TheValue::Text("".into()));
2562                        insert_content_into_maps(&mut self.project);
2563                        update_server_icons = true;
2564
2565                        ctx.ui.send(TheEvent::Custom(
2566                            TheId::named("Render SceneManager Map"),
2567                            TheValue::Empty,
2568                        ));
2569                    } else if id.name == "Undo" || id.name == "Redo" {
2570                        let mut refresh_action_ui = false;
2571                        if ui.focus_widget_supports_undo_redo(ctx) {
2572                            if id.name == "Undo" {
2573                                ui.undo(ctx);
2574                            } else {
2575                                ui.redo(ctx);
2576                            }
2577                        } else if DOCKMANAGER.read().unwrap().current_dock_supports_undo() {
2578                            if id.name == "Undo" {
2579                                DOCKMANAGER.write().unwrap().undo(
2580                                    ui,
2581                                    ctx,
2582                                    &mut self.project,
2583                                    &mut self.server_ctx,
2584                                );
2585                            } else {
2586                                DOCKMANAGER.write().unwrap().redo(
2587                                    ui,
2588                                    ctx,
2589                                    &mut self.project,
2590                                    &mut self.server_ctx,
2591                                );
2592                            }
2593                            refresh_action_ui = true;
2594                        } else {
2595                            let mut manager = UNDOMANAGER.write().unwrap();
2596
2597                            if id.name == "Undo" {
2598                                manager.undo(&mut self.server_ctx, &mut self.project, ui, ctx);
2599                            } else {
2600                                manager.redo(&mut self.server_ctx, &mut self.project, ui, ctx);
2601                            }
2602                            refresh_action_ui = true;
2603                        }
2604
2605                        // Keep action list and TOML params in sync only when project/dock state changed.
2606                        if refresh_action_ui {
2607                            // Drop focus to avoid stale focused text-edit state surviving toolbar rebuilds.
2608                            ctx.ui.clear_focus();
2609                            // Refresh both toolbars unconditionally.
2610                            // Dock undo/redo may not keep CODEEDITOR.active_panel in sync.
2611                            {
2612                                let mut module = CODEGRIDFX.write().unwrap();
2613                                module.clear_toolbar_settings(ui, ctx);
2614                                module.show_settings(ui, ctx);
2615                            }
2616                            ctx.ui.send(TheEvent::Custom(
2617                                TheId::named("Update Action List"),
2618                                TheValue::Empty,
2619                            ));
2620                            ctx.ui.send(TheEvent::Custom(
2621                                TheId::named("Update Action Parameters"),
2622                                TheValue::Empty,
2623                            ));
2624                        }
2625                    } else if id.name == "Cut" {
2626                        if ui.focus_widget_supports_clipboard(ctx) {
2627                            // Widget specific
2628                            ui.cut(ctx);
2629                        } else {
2630                            // Global
2631                            ctx.ui.send(TheEvent::Cut);
2632                        }
2633                    } else if id.name == "Copy" {
2634                        if ui.focus_widget_supports_clipboard(ctx) {
2635                            // Widget specific
2636                            ui.copy(ctx);
2637                        } else {
2638                            // Global
2639                            ctx.ui.send(TheEvent::Copy);
2640                        }
2641                    } else if id.name == "Paste" {
2642                        if ui.focus_widget_supports_clipboard(ctx) {
2643                            // Widget specific
2644                            ui.paste(ctx);
2645                        } else {
2646                            // Global
2647                            if let Some(value) = &ctx.ui.clipboard {
2648                                ctx.ui.send(TheEvent::Paste(
2649                                    value.clone(),
2650                                    ctx.ui.clipboard_app_type.clone(),
2651                                ));
2652                            } else {
2653                                ctx.ui.send(TheEvent::Paste(
2654                                    TheValue::Empty,
2655                                    ctx.ui.clipboard_app_type.clone(),
2656                                ));
2657                            }
2658                        }
2659                    }
2660                }
2661                TheEvent::ValueChanged(id, value) => {
2662                    if id.name == "Server Time Slider" {
2663                        if let TheValue::Time(time) = value {
2664                            self.project.time = time;
2665                            let mut rusterix = RUSTERIX.write().unwrap();
2666                            rusterix.client.set_server_time(time);
2667
2668                            if rusterix.server.state == rusterix::ServerState::Running {
2669                                if let Some(map) = self.project.get_map(&self.server_ctx) {
2670                                    rusterix.server.set_time(&map.id, time);
2671                                }
2672                            }
2673                        }
2674                    }
2675                }
2676                _ => {}
2677            }
2678        }
2679
2680        #[cfg(all(not(target_arch = "wasm32"), feature = "self-update"))]
2681        while let Ok(event) = self.self_update_rx.try_recv() {
2682            match event {
2683                SelfUpdateEvent::AlreadyUpToDate => {
2684                    let text = str!("Eldiron is already up-to-date.");
2685                    let uuid = Uuid::new_v4();
2686
2687                    let width = 300;
2688                    let height = 100;
2689
2690                    let mut canvas = TheCanvas::new();
2691                    canvas.limiter_mut().set_max_size(Vec2::new(width, height));
2692
2693                    let mut hlayout: TheHLayout = TheHLayout::new(TheId::empty());
2694                    hlayout.limiter_mut().set_max_width(width);
2695
2696                    let mut text_widget = TheText::new(TheId::named_with_id("Dialog Value", uuid));
2697                    text_widget.set_text(text.to_string());
2698                    text_widget.limiter_mut().set_max_width(200);
2699                    hlayout.add_widget(Box::new(text_widget));
2700
2701                    canvas.set_layout(hlayout);
2702
2703                    ui.show_dialog(
2704                        "Eldiron Up-to-Date",
2705                        canvas,
2706                        vec![TheDialogButtonRole::Accept],
2707                        ctx,
2708                    );
2709                }
2710                SelfUpdateEvent::UpdateCompleted(release) => {
2711                    if let Some(statusbar) = ui.get_widget("Statusbar") {
2712                        statusbar.as_statusbar().unwrap().set_text(format!(
2713                            "Updated to version {}. Please restart the application to enjoy the new features.",
2714                            release.version
2715                        ));
2716                    }
2717                }
2718                SelfUpdateEvent::UpdateConfirm(release) => {
2719                    let text = &format!("Update to version {}?", release.version);
2720                    let uuid = Uuid::new_v4();
2721
2722                    let width = 300;
2723                    let height = 100;
2724
2725                    let mut canvas = TheCanvas::new();
2726                    canvas.limiter_mut().set_max_size(Vec2::new(width, height));
2727
2728                    let mut hlayout: TheHLayout = TheHLayout::new(TheId::empty());
2729                    hlayout.limiter_mut().set_max_width(width);
2730
2731                    let mut text_widget = TheText::new(TheId::named_with_id("Dialog Value", uuid));
2732                    text_widget.set_text(text.to_string());
2733                    text_widget.limiter_mut().set_max_width(200);
2734                    hlayout.add_widget(Box::new(text_widget));
2735
2736                    canvas.set_layout(hlayout);
2737
2738                    ui.show_dialog(
2739                        "Update Eldiron",
2740                        canvas,
2741                        vec![TheDialogButtonRole::Accept, TheDialogButtonRole::Reject],
2742                        ctx,
2743                    );
2744                }
2745                SelfUpdateEvent::UpdateError(err) => {
2746                    if let Some(statusbar) = ui.get_widget("Statusbar") {
2747                        statusbar
2748                            .as_statusbar()
2749                            .unwrap()
2750                            .set_text(format!("Failed to update Eldiron: {err}"));
2751                    }
2752                }
2753                SelfUpdateEvent::UpdateStart(release) => {
2754                    if let Some(statusbar) = ui.get_widget("Statusbar") {
2755                        statusbar
2756                            .as_statusbar()
2757                            .unwrap()
2758                            .set_text(format!("Updating to version {}...", release.version));
2759                    }
2760                }
2761            }
2762        }
2763
2764        if update_server_icons {
2765            self.update_server_state_icons(ui);
2766            redraw = true;
2767        }
2768
2769        let active_dirty = UNDOMANAGER.read().unwrap().has_unsaved()
2770            || DOCKMANAGER.read().unwrap().has_dock_changes();
2771        if self.active_session < self.sessions.len()
2772            && self.sessions[self.active_session].dirty != active_dirty
2773        {
2774            self.sessions[self.active_session].dirty = active_dirty;
2775            self.rebuild_project_tabs(ui);
2776            redraw = true;
2777        }
2778        if active_dirty != self.last_active_dirty {
2779            self.last_active_dirty = active_dirty;
2780            self.rebuild_project_tabs(ui);
2781            redraw = true;
2782        }
2783
2784        self.update_counter += 1;
2785        if self.update_counter > 2 {
2786            self.sidebar.startup = false;
2787        }
2788        redraw
2789    }
2790
2791    /// Returns true if there are changes
2792    fn has_changes(&self) -> bool {
2793        if self.active_session_has_changes() {
2794            return true;
2795        }
2796
2797        for (index, session) in self.sessions.iter().enumerate() {
2798            if index != self.active_session && session.dirty {
2799                return true;
2800            }
2801        }
2802
2803        false
2804    }
2805
2806    fn window_moved(&mut self, x: i32, y: i32) {
2807        self.window_state.x = Some(x);
2808        self.window_state.y = Some(y);
2809        self.save_window_state();
2810    }
2811
2812    fn window_resized(&mut self, width: usize, height: usize) {
2813        if width > 0 && height > 0 {
2814            self.window_state.width = Some(width);
2815            self.window_state.height = Some(height);
2816            self.save_window_state();
2817        }
2818    }
2819}
2820
2821pub trait EldironEditor {
2822    fn update_server_state_icons(&mut self, ui: &mut TheUI);
2823}
2824
2825impl EldironEditor for Editor {
2826    fn update_server_state_icons(&mut self, ui: &mut TheUI) {
2827        let rusterix = RUSTERIX.read().unwrap();
2828        if rusterix.server.state == rusterix::ServerState::Running {
2829            if let Some(button) = ui.get_widget("Play") {
2830                if let Some(button) = button.as_menubar_button() {
2831                    button.set_icon_name("play-fill".to_string());
2832                }
2833            }
2834            if let Some(button) = ui.get_widget("Pause") {
2835                if let Some(button) = button.as_menubar_button() {
2836                    button.set_icon_name("play-pause".to_string());
2837                }
2838            }
2839            if let Some(button) = ui.get_widget("Stop") {
2840                if let Some(button) = button.as_menubar_button() {
2841                    button.set_icon_name("stop".to_string());
2842                }
2843            }
2844        } else if rusterix.server.state == rusterix::ServerState::Paused {
2845            if let Some(button) = ui.get_widget("Play") {
2846                if let Some(button) = button.as_menubar_button() {
2847                    button.set_icon_name("play".to_string());
2848                }
2849            }
2850            if let Some(button) = ui.get_widget("Pause") {
2851                if let Some(button) = button.as_menubar_button() {
2852                    button.set_icon_name("play-pause-fill".to_string());
2853                }
2854            }
2855            if let Some(button) = ui.get_widget("Stop") {
2856                if let Some(button) = button.as_menubar_button() {
2857                    button.set_icon_name("stop".to_string());
2858                }
2859            }
2860        } else if rusterix.server.state == rusterix::ServerState::Off {
2861            if let Some(button) = ui.get_widget("Play") {
2862                if let Some(button) = button.as_menubar_button() {
2863                    button.set_icon_name("play".to_string());
2864                }
2865            }
2866            if let Some(button) = ui.get_widget("Pause") {
2867                if let Some(button) = button.as_menubar_button() {
2868                    button.set_icon_name("play-pause".to_string());
2869                }
2870            }
2871            if let Some(button) = ui.get_widget("Stop") {
2872                if let Some(button) = button.as_menubar_button() {
2873                    button.set_icon_name("stop-fill".to_string());
2874                }
2875            }
2876        }
2877    }
2878}