Skip to main content

quaso/editor/
mod.rs

1pub mod features;
2pub mod ui;
3
4use crate::{
5    context::GameContext,
6    editor::features::viewport::{EditorGameViewport, editor_viewport_game_world_and_ui},
7    game::GameGlobals,
8    third_party::windowing::event::{
9        ElementState, Event, ModifiersState, MouseScrollDelta, WindowEvent,
10    },
11};
12use fontdue::Font;
13use raui_core::{
14    interactive::default_interactions_engine::{Interaction, PointerButton},
15    layout::CoordsMapping,
16    widget::{
17        component::interactive::navigation::{NavJump, NavScroll, NavSignal, NavTextChange},
18        node::WidgetNode,
19        unit::text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign},
20        utils::{Rect, Vec2},
21    },
22};
23use raui_immediate::{ImSharedProps, apply, begin, end};
24use raui_immediate_widgets::material::containers::nav_paper;
25use raui_material::theme::{ThemeProps, ThemedTextMaterial, new_dark_theme};
26use spitfire_draw::{canvas::Canvas, context::DrawContext, utils::Vertex};
27use spitfire_glow::{graphics::Graphics, renderer::GlowTextureFormat};
28use spitfire_gui::context::GuiContext;
29use spitfire_input::{InputContext, InputMapping, InputMappingRef, MouseButton, VirtualKeyCode};
30use std::{
31    any::{Any, TypeId},
32    borrow::Cow,
33    collections::HashMap,
34    sync::mpsc::{Receiver, Sender},
35};
36use typid::ID;
37
38const ROBOTO_FONT_DATA: &[u8] = include_bytes!("./roboto.ttf");
39pub const EDITOR_FONT_NAME: &str = "~~editor-roboto-font~~";
40
41pub struct Editor {
42    pub(crate) subsystems: Vec<Box<dyn EditorSubsystem>>,
43    game_canvas: Option<Canvas>,
44    game_widgets: Vec<WidgetNode>,
45    #[allow(clippy::type_complexity)]
46    gui_drawer: Box<dyn FnMut(&mut GameContext, &mut EditorSubsystems)>,
47    edit_mode_switch_key: VirtualKeyCode,
48    show_editor_while_running: bool,
49    coords_mapping: CoordsMapping,
50}
51
52impl Default for Editor {
53    fn default() -> Self {
54        Self {
55            subsystems: Default::default(),
56            game_canvas: None,
57            game_widgets: Default::default(),
58            gui_drawer: Box::new(editor_viewport_game_world_and_ui),
59            edit_mode_switch_key: VirtualKeyCode::F5,
60            show_editor_while_running: true,
61            coords_mapping: Default::default(),
62        }
63    }
64}
65
66impl Editor {
67    pub fn with_gui_drawer(mut self, f: fn(&mut GameContext, &mut EditorSubsystems)) -> Self {
68        self.gui_drawer = Box::new(f);
69        self
70    }
71
72    pub fn show_editor_while_running(mut self, show: bool) -> Self {
73        self.show_editor_while_running = show;
74        self
75    }
76
77    pub(crate) fn initialize(&mut self, context: GameContext) {
78        context.draw.fonts.insert(
79            EDITOR_FONT_NAME,
80            Font::from_bytes(ROBOTO_FONT_DATA, Default::default()).unwrap(),
81        );
82    }
83
84    pub(crate) fn begin_frame_capture(
85        &mut self,
86        graphics: &mut Graphics<Vertex>,
87        draw: &mut DrawContext,
88    ) {
89        if let Some(canvas) = &mut self.game_canvas {
90            canvas.surface_mut().set_color(graphics.state.color);
91            canvas.activate(draw, graphics, true);
92        } else {
93            self.game_canvas = Canvas::from_screen(vec![GlowTextureFormat::Rgb], graphics).ok();
94            if let Some(canvas) = &self.game_canvas {
95                canvas.activate(draw, graphics, true);
96            }
97        }
98    }
99
100    pub(crate) fn end_frame_capture(
101        &mut self,
102        graphics: &mut Graphics<Vertex>,
103        draw: &mut DrawContext,
104    ) {
105        if let Some(canvas) = &self.game_canvas {
106            Canvas::deactivate(draw, graphics);
107            draw.textures.insert(
108                EditorGameViewport::ID.into(),
109                canvas.surface().attachments()[0].texture.clone(),
110            );
111        }
112    }
113
114    pub(crate) fn begin_gui_capture(&mut self) {
115        begin();
116    }
117
118    pub(crate) fn end_gui_capture(&mut self) {
119        self.game_widgets = end();
120    }
121
122    pub(crate) fn update(
123        &mut self,
124        graphics: &mut Graphics<Vertex>,
125        gui: &GuiContext,
126        globals: &mut GameGlobals,
127    ) {
128        globals.editor.input.maintain();
129        globals.editor.viewport_rectangle = Default::default();
130        if let Some(canvas) = &mut self.game_canvas
131            && let Some((_, layout)) = gui
132                .application
133                .layout_data()
134                .items
135                .iter()
136                .find(|(id, _)| id.key() == EditorGameViewport::ID)
137        {
138            self.coords_mapping = CoordsMapping::new_scaling(
139                Rect {
140                    left: 0.0,
141                    right: graphics.state.main_camera.screen_size.x,
142                    top: 0.0,
143                    bottom: graphics.state.main_camera.screen_size.y,
144                },
145                gui.coords_map_scaling,
146            );
147            let layout = layout.virtual_to_real(&self.coords_mapping);
148            globals.editor.viewport_rectangle.x = layout.ui_space.left;
149            globals.editor.viewport_rectangle.y = layout.ui_space.top;
150            let width = layout.ui_space.width() as u32;
151            let height = layout.ui_space.height() as u32;
152            let _ = canvas.match_to_size(graphics, width.max(1), height.max(1));
153            globals.editor.viewport_rectangle.w = layout.ui_space.width();
154            globals.editor.viewport_rectangle.h = layout.ui_space.height();
155        }
156    }
157
158    pub(crate) fn draw_gui(&mut self, mut context: GameContext) {
159        let mut viewport = context.globals.ensure::<EditorGameViewport>();
160        viewport.write().widgets.clear();
161        viewport.write().widgets.append(&mut self.game_widgets);
162        if self.show_editor_while_running || context.globals.editor.is_editing() {
163            apply(ImSharedProps(make_theme()), || {
164                nav_paper((), || {
165                    (self.gui_drawer)(
166                        &mut context,
167                        &mut EditorSubsystems {
168                            subsystems: &mut self.subsystems,
169                        },
170                    );
171                });
172            });
173        } else {
174            viewport.write().world_and_ui(Default::default());
175        }
176    }
177
178    pub fn event(&mut self, event: &WindowEvent, gui: &mut GuiContext, globals: &mut GameGlobals) {
179        if let WindowEvent::KeyboardInput { input, .. } = event
180            && input.virtual_keycode == Some(self.edit_mode_switch_key)
181            && input.state == ElementState::Pressed
182        {
183            globals.editor.is_editing = !globals.editor.is_editing;
184        }
185
186        match event {
187            WindowEvent::ModifiersChanged(modifiers) => {
188                globals.editor.input.modifiers = *modifiers;
189            }
190            WindowEvent::ReceivedCharacter(character) => {
191                gui.interactions
192                    .engine
193                    .interact(Interaction::Navigate(NavSignal::TextChange(
194                        NavTextChange::InsertCharacter(*character),
195                    )));
196            }
197            WindowEvent::CursorMoved { position, .. } => {
198                globals.editor.input.pointer_position = self.coords_mapping.real_to_virtual_vec2(
199                    Vec2 {
200                        x: position.x as _,
201                        y: position.y as _,
202                    },
203                    false,
204                );
205                gui.interactions.engine.interact(Interaction::PointerMove(
206                    globals.editor.input.pointer_position,
207                ));
208            }
209            WindowEvent::MouseWheel { delta, .. } => {
210                let value = match delta {
211                    MouseScrollDelta::LineDelta(x, y) => Vec2 {
212                        x: -globals.editor.input.single_scroll_units.x * *x,
213                        y: -globals.editor.input.single_scroll_units.y * *y,
214                    },
215                    MouseScrollDelta::PixelDelta(delta) => Vec2 {
216                        x: -delta.x as _,
217                        y: -delta.y as _,
218                    },
219                };
220                gui.interactions
221                    .engine
222                    .interact(Interaction::Navigate(NavSignal::Jump(NavJump::Scroll(
223                        NavScroll::Units(value, true),
224                    ))));
225            }
226            WindowEvent::MouseInput { state, button, .. } => match state {
227                ElementState::Pressed => match button {
228                    MouseButton::Left => {
229                        gui.interactions.engine.interact(Interaction::PointerDown(
230                            PointerButton::Trigger,
231                            globals.editor.input.pointer_position,
232                        ));
233                    }
234                    MouseButton::Right => {
235                        gui.interactions.engine.interact(Interaction::PointerDown(
236                            PointerButton::Context,
237                            globals.editor.input.pointer_position,
238                        ));
239                    }
240                    _ => {}
241                },
242                ElementState::Released => match button {
243                    MouseButton::Left => {
244                        gui.interactions.engine.interact(Interaction::PointerUp(
245                            PointerButton::Trigger,
246                            globals.editor.input.pointer_position,
247                        ));
248                    }
249                    MouseButton::Right => {
250                        gui.interactions.engine.interact(Interaction::PointerUp(
251                            PointerButton::Context,
252                            globals.editor.input.pointer_position,
253                        ));
254                    }
255                    _ => {}
256                },
257            },
258            WindowEvent::KeyboardInput { input, .. } => {
259                if input.state == ElementState::Pressed {
260                    if let Some(key) = input.virtual_keycode {
261                        if gui.interactions.engine.focused_text_input().is_some() {
262                            match key {
263                                VirtualKeyCode::Left => {
264                                    gui.interactions.engine.interact(Interaction::Navigate(
265                                        NavSignal::TextChange(NavTextChange::MoveCursorLeft),
266                                    ))
267                                }
268                                VirtualKeyCode::Right => {
269                                    gui.interactions.engine.interact(Interaction::Navigate(
270                                        NavSignal::TextChange(NavTextChange::MoveCursorRight),
271                                    ))
272                                }
273                                VirtualKeyCode::Home => {
274                                    gui.interactions.engine.interact(Interaction::Navigate(
275                                        NavSignal::TextChange(NavTextChange::MoveCursorStart),
276                                    ))
277                                }
278                                VirtualKeyCode::End => {
279                                    gui.interactions.engine.interact(Interaction::Navigate(
280                                        NavSignal::TextChange(NavTextChange::MoveCursorEnd),
281                                    ))
282                                }
283                                VirtualKeyCode::Back => {
284                                    gui.interactions.engine.interact(Interaction::Navigate(
285                                        NavSignal::TextChange(NavTextChange::DeleteLeft),
286                                    ))
287                                }
288                                VirtualKeyCode::Delete => {
289                                    gui.interactions.engine.interact(Interaction::Navigate(
290                                        NavSignal::TextChange(NavTextChange::DeleteRight),
291                                    ))
292                                }
293                                VirtualKeyCode::Return | VirtualKeyCode::NumpadEnter => {
294                                    gui.interactions.engine.interact(Interaction::Navigate(
295                                        NavSignal::TextChange(NavTextChange::NewLine),
296                                    ))
297                                }
298                                VirtualKeyCode::Escape => {
299                                    gui.interactions.engine.interact(Interaction::Navigate(
300                                        NavSignal::FocusTextInput(().into()),
301                                    ));
302                                }
303                                _ => {}
304                            }
305                        } else {
306                            match key {
307                                VirtualKeyCode::Up => gui
308                                    .interactions
309                                    .engine
310                                    .interact(Interaction::Navigate(NavSignal::Up)),
311                                VirtualKeyCode::Down => gui
312                                    .interactions
313                                    .engine
314                                    .interact(Interaction::Navigate(NavSignal::Down)),
315                                VirtualKeyCode::Left => {
316                                    if globals.editor.input.modifiers.shift() {
317                                        gui.interactions
318                                            .engine
319                                            .interact(Interaction::Navigate(NavSignal::Prev));
320                                    } else {
321                                        gui.interactions
322                                            .engine
323                                            .interact(Interaction::Navigate(NavSignal::Left));
324                                    }
325                                }
326                                VirtualKeyCode::Right => {
327                                    if globals.editor.input.modifiers.shift() {
328                                        gui.interactions
329                                            .engine
330                                            .interact(Interaction::Navigate(NavSignal::Next));
331                                    } else {
332                                        gui.interactions
333                                            .engine
334                                            .interact(Interaction::Navigate(NavSignal::Right));
335                                    }
336                                }
337                                VirtualKeyCode::Return
338                                | VirtualKeyCode::NumpadEnter
339                                | VirtualKeyCode::Space => {
340                                    gui.interactions
341                                        .engine
342                                        .interact(Interaction::Navigate(NavSignal::Accept(true)));
343                                }
344                                VirtualKeyCode::Escape | VirtualKeyCode::Back => {
345                                    gui.interactions
346                                        .engine
347                                        .interact(Interaction::Navigate(NavSignal::Cancel(true)));
348                                }
349                                _ => {}
350                            }
351                        }
352                    }
353                } else if input.state == ElementState::Released
354                    && let Some(key) = input.virtual_keycode
355                    && gui.interactions.engine.focused_text_input().is_none()
356                {
357                    match key {
358                        VirtualKeyCode::Return
359                        | VirtualKeyCode::NumpadEnter
360                        | VirtualKeyCode::Space => {
361                            gui.interactions
362                                .engine
363                                .interact(Interaction::Navigate(NavSignal::Accept(false)));
364                        }
365                        VirtualKeyCode::Escape | VirtualKeyCode::Back => {
366                            gui.interactions
367                                .engine
368                                .interact(Interaction::Navigate(NavSignal::Cancel(false)));
369                        }
370                        _ => {}
371                    }
372                }
373            }
374            _ => {}
375        }
376    }
377}
378
379fn make_theme() -> ThemeProps {
380    new_dark_theme()
381        .text_variant(
382            "XXL",
383            ThemedTextMaterial {
384                font: TextBoxFont {
385                    name: EDITOR_FONT_NAME.to_string(),
386                    size: 48.0,
387                },
388                ..Default::default()
389            },
390        )
391        .text_variant(
392            "XL",
393            ThemedTextMaterial {
394                font: TextBoxFont {
395                    name: EDITOR_FONT_NAME.to_string(),
396                    size: 32.0,
397                },
398                ..Default::default()
399            },
400        )
401        .text_variant(
402            "L",
403            ThemedTextMaterial {
404                font: TextBoxFont {
405                    name: EDITOR_FONT_NAME.to_string(),
406                    size: 24.0,
407                },
408                ..Default::default()
409            },
410        )
411        .text_variant(
412            "",
413            ThemedTextMaterial {
414                font: TextBoxFont {
415                    name: EDITOR_FONT_NAME.to_string(),
416                    size: 18.0,
417                },
418                ..Default::default()
419            },
420        )
421        .text_variant(
422            "S",
423            ThemedTextMaterial {
424                font: TextBoxFont {
425                    name: EDITOR_FONT_NAME.to_string(),
426                    size: 14.0,
427                },
428                ..Default::default()
429            },
430        )
431        .text_variant(
432            "XS",
433            ThemedTextMaterial {
434                font: TextBoxFont {
435                    name: EDITOR_FONT_NAME.to_string(),
436                    size: 10.0,
437                },
438                ..Default::default()
439            },
440        )
441        .text_variant(
442            "XXS",
443            ThemedTextMaterial {
444                font: TextBoxFont {
445                    name: EDITOR_FONT_NAME.to_string(),
446                    size: 6.0,
447                },
448                ..Default::default()
449            },
450        )
451        .text_variant(
452            "button",
453            ThemedTextMaterial {
454                font: TextBoxFont {
455                    name: EDITOR_FONT_NAME.to_string(),
456                    size: 18.0,
457                },
458                horizontal_align: TextBoxHorizontalAlign::Center,
459                vertical_align: TextBoxVerticalAlign::Middle,
460                ..Default::default()
461            },
462        )
463}
464
465pub enum EditorInputCommand {
466    AddMapping {
467        name: Cow<'static, str>,
468        mapping: InputMappingRef,
469    },
470    RemoveMapping {
471        name: Cow<'static, str>,
472    },
473}
474
475pub struct EditorInput {
476    pub(crate) single_scroll_units: Vec2,
477    pub(crate) pointer_position: Vec2,
478    pub(crate) modifiers: ModifiersState,
479    pub(crate) context: InputContext,
480    table: HashMap<Cow<'static, str>, ID<InputMapping>>,
481    sender: Sender<EditorInputCommand>,
482    receiver: Receiver<EditorInputCommand>,
483}
484
485impl Default for EditorInput {
486    fn default() -> Self {
487        let (sender, receiver) = std::sync::mpsc::channel();
488        Self {
489            single_scroll_units: Vec2 { x: 10.0, y: 0.0 },
490            pointer_position: Default::default(),
491            modifiers: Default::default(),
492            context: Default::default(),
493            table: Default::default(),
494            sender,
495            receiver,
496        }
497    }
498}
499
500impl EditorInput {
501    pub fn commands(&self) -> &Sender<EditorInputCommand> {
502        &self.sender
503    }
504
505    pub(crate) fn maintain(&mut self) {
506        while let Ok(command) = self.receiver.try_recv() {
507            match command {
508                EditorInputCommand::AddMapping { name, mapping } => {
509                    let id = self.context.push_mapping(mapping.clone());
510                    self.table.insert(name, id);
511                }
512                EditorInputCommand::RemoveMapping { name } => {
513                    if let Some(id) = self.table.remove(&name) {
514                        self.context.remove_mapping(id);
515                    }
516                }
517            }
518        }
519        self.context.maintain();
520    }
521}
522
523pub trait EditorSubsystem {
524    #[allow(unused_variables)]
525    fn update(&mut self, context: GameContext, delta_time: f32) {}
526
527    #[allow(unused_variables)]
528    fn fixed_update(&mut self, context: GameContext, delta_time: f32) {}
529
530    #[allow(unused_variables)]
531    fn draw(&mut self, context: GameContext) {}
532
533    #[allow(unused_variables)]
534    fn draw_gui(&mut self, context: GameContext) {}
535
536    #[allow(unused_variables)]
537    fn event(&mut self, globals: &mut GameGlobals, event: &Event<()>) {}
538
539    fn as_any(&self) -> &dyn Any;
540
541    fn as_any_mut(&mut self) -> &mut dyn Any;
542}
543
544pub struct EditorSubsystems<'a> {
545    subsystems: &'a mut Vec<Box<dyn EditorSubsystem>>,
546}
547
548impl<'a> EditorSubsystems<'a> {
549    pub fn add<T: EditorSubsystem + 'static>(&mut self, subsystem: T) {
550        self.remove::<T>();
551        self.subsystems.push(Box::new(subsystem));
552    }
553
554    pub fn remove<T: EditorSubsystem + 'static>(&mut self) {
555        self.subsystems
556            .retain(|s| s.as_any().type_id() != TypeId::of::<T>());
557    }
558
559    pub fn ensure<T: EditorSubsystem + Default + 'static>(&mut self) -> &mut T {
560        if self
561            .subsystems
562            .iter()
563            .all(|s| s.as_any().type_id() != TypeId::of::<T>())
564        {
565            self.subsystems.push(Box::new(T::default()));
566        }
567        self.get_mut::<T>().unwrap()
568    }
569
570    pub fn get<T: EditorSubsystem + 'static>(&self) -> Option<&T> {
571        for subsystem in self.subsystems.iter() {
572            if let Some(specific) = subsystem.as_any().downcast_ref::<T>() {
573                return Some(specific);
574            }
575        }
576        None
577    }
578
579    pub fn get_mut<T: EditorSubsystem + 'static>(&mut self) -> Option<&mut T> {
580        for subsystem in self.subsystems.iter_mut() {
581            if let Some(specific) = subsystem.as_any_mut().downcast_mut::<T>() {
582                return Some(specific);
583            }
584        }
585        None
586    }
587}