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}