Skip to main content

gl_utils/render/resource/
demo.rs

1use std::sync::{Arc, LazyLock, RwLock};
2use strum::IntoEnumIterator;
3use winit;
4
5use math_utils as math;
6use math_utils::num;
7
8use crate::{render, tile, Render};
9use super::*;
10
11/// Winit deprecated the modifiers field of keyboard input so we have to track modifier
12/// changes
13static MODIFIERS : LazyLock <Arc <RwLock <winit::keyboard::ModifiersState>>> =
14  LazyLock::new (|| Arc::new (RwLock::new (winit::keyboard::ModifiersState::empty())));
15
16/// A type to implement winit `ApplicationHandler` trait
17pub struct App {
18  pub render             : Render <Default>,
19  pub mouse_position     : (f64, f64),
20  pub mouse_button_event : Option <winit::event::ElementState>,
21  pub running            : bool
22}
23
24impl App {
25  pub fn new (
26    glium_display : glium::Display <glutin::surface::WindowSurface>,
27    window        : winit::window::Window
28  ) -> Self {
29    let mut render = Render::<Default>::new (glium_display, window);
30    render.demo_init();
31    let mouse_position      = (0.0, 0.0);
32    let mouse_button_event  = None;
33    let running             = true;
34    Self { render, mouse_position, mouse_button_event, running }
35  }
36}
37
38impl winit::application::ApplicationHandler for App {
39  // required
40  fn resumed (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) { }
41
42  fn window_event (&mut self,
43    _event_loop : &winit::event_loop::ActiveEventLoop,
44    _window_id  : winit::window::WindowId,
45    event       : winit::event::WindowEvent
46  ) {
47    // winit events:
48    // TODO: review
49    // - window events includes input events received by the window
50    // - device events are received independent of window focus
51    // there may be differences in the events received depending on platform:
52    // - on Linux keyboard input is received as both a device and window event
53    // - on Windows only mouse motion device events are received, all other
54    //   input is received as window events
55    self.render.demo_handle_winit_window_event (
56      event,
57      &mut self.running,
58      &mut self.mouse_position,
59      &mut self.mouse_button_event)
60  }
61}
62
63impl Render <Default> {
64  /// Get current keyboard modifiers
65  pub fn modifiers() -> winit::keyboard::ModifiersState {
66    *MODIFIERS.read().unwrap()
67  }
68  /// Initializes "demo" state such as example mesh instances and viewport text
69  /// tiles.
70  ///
71  /// This should normally be called immediately after render context creation
72  /// as some assumptions are made about the current state (see Panics below).
73  /// The renderer can be returned to this state with the `reset` method.
74  ///
75  /// After `demo_init`, `demo_handle_winit_event` can be called on incoming
76  /// events to interact with the demo.
77  ///
78  /// A usage example is provided in `./examples/demo.rs`.
79  ///
80  /// # Panics
81  ///
82  /// A prerequisite is that the first four `viewport_resources` entries are
83  /// empty.
84  ///
85  /// The currently allocated per-instance buffers should be empty.
86  // TODO: doctests, make safe to call from any state?
87  fn demo_init (&mut self) {
88    use color::Glsl;
89    use draw3d::*;
90
91    println!(">>> Initializing gl-utils demo...");
92    println!("  Press 'Q' or 'Esc' to quit");
93    println!("  Horizontal movement: 'W', 'A', 'S', 'D'");
94    println!("  Vertical movement: 'Space', 'LCtrl' or 'R', 'F'");
95    println!("  Rotation: 'Up', 'Down', 'Left', 'Right' or 'H', 'J', 'K', 'L'");
96    println!("  Zoom in/out 3D (perspective): 'I', 'O'");
97    println!("  Zoom in/out 3D (orthographic): 'Alt+I', 'Alt+O'");
98    println!("  Zoom in/out 2D: 'Shift+Alt+I', 'Shift+Alt+O'");
99    println!("  Toggle split into 4 viewports with orthographic views: 'X'");
100    println!("  Screenshot: 'F10'");
101
102    // set pointer texture
103    self.resource.draw2d.draw_pointer =
104      Some (DefaultTexturePointerId::Hand as PointerTextureIndexRepr);
105    // hide hardware cursor
106    self.window.set_cursor_visible (false);
107
108    // light
109    self.resource.draw3d.light_directional =
110      math::Unit3::normalize ([1.0, 1.0, -1.0].into());
111
112    //
113    // 3D per-instance data
114    //
115    // cube vertex data
116    let cube_vertex = [vertex::Vert3dOrientationScaleColor {
117      position:    [-1.5, 2.5, 1.0],
118      orientation: math::Matrix3::identity().into_col_arrays(),
119      scale:       [std::f32::consts::SQRT_3 / 6.0; 3],
120      color:       color::CYAN.glsl_rgba()
121    }];
122    // aabb lines vertex data
123    let aabb_lines_vertex = [vertex::Vert3dOrientationScaleColor {
124      position:    [-0.5, 2.5, 1.0],
125      orientation: math::Matrix3::identity().into_col_arrays(),
126      scale:       [0.5, 0.5, 1.0],
127      color:       color::DEBUG_PINK.glsl_rgba()
128    }];
129    // aabb triangles vertex data
130    let aabb_triangles_vertex = [vertex::Vert3dOrientationScaleColor {
131      color: color::DEBUG_GREY.glsl_rgba(),
132      .. aabb_lines_vertex[0]
133    }];
134    // shaded spheres vertex data
135    let spheres_raycast_vertices = [
136      vertex::Vert3dOrientationScaleColor {
137        position:    [-1.5, 2.5, 3.0],
138        orientation: math::Matrix3::identity().into_col_arrays(),
139        scale:       [0.5, 0.5, 0.5],
140        color:
141          color::hue_luminance_custom (0.0, 44.0).glsl_rgba()
142      },
143      vertex::Vert3dOrientationScaleColor {
144        position:    [0.0, 2.5, 3.0],
145        orientation: math::Matrix3::identity().into_col_arrays(),
146        scale:       [0.5, 0.5, 0.5],
147        color:
148          color::hue_luminance_custom (120.0, 44.0).glsl_rgba()
149      },
150      vertex::Vert3dOrientationScaleColor {
151        position:    [1.5, 2.5, 3.0],
152        orientation: math::Matrix3::identity().into_col_arrays(),
153        scale:       [0.5, 0.5, 0.5],
154        color:
155          color::hue_luminance_custom (240.0, 44.0).glsl_rgba()
156      }
157    ];
158    // grid vertex data
159    let grid_vertex_data = Default::debug_grid_vertices();
160    // hemisphere vertex data
161    let hemisphere_vertex = [vertex::Vert3dOrientationScaleColor {
162      position:    [2.5, 2.5, 0.0],
163      orientation: math::Matrix3::identity().into_col_arrays(),
164      scale:       [0.5, 0.5, 0.5],
165      color:       color::DEBUG_VIOLET.glsl_rgba()
166    }];
167    // sphere vertex data
168    // sphere: 1.0 diameter
169    let sphere_vertex = [vertex::Vert3dOrientationScaleColor {
170      position:    [1.5, 2.5, 0.5],
171      orientation: math::Matrix3::identity().into_col_arrays(),
172      scale:       [0.5, 0.5, 0.5],
173      color:       color::DEBUG_RED.glsl_rgba()
174    }];
175    // capsule vertex data
176    let capsule_vertex = [vertex::Vert3dOrientationScaleColor {
177      position:    [0.5, 2.5, 1.0],
178      orientation: math::Matrix3::identity().into_col_arrays(),
179      scale:       [0.5, 0.5, 1.0],
180      color:       color::DEBUG_AZURE.glsl_rgba()
181    }];
182    // cylinder vertex data
183    let cylinder_vertex = [vertex::Vert3dOrientationScaleColor {
184      position:    [3.5, 2.5, 1.0],
185      orientation: math::Matrix3::identity().into_col_arrays(),
186      scale:       [0.5, 0.5, 1.0],
187      color:       color::DEBUG_LIGHT_BLUE.glsl_rgba()
188    }];
189    let aabb_lines               = Some (&aabb_lines_vertex[..]);
190    let aabb_triangles           = Some (&aabb_triangles_vertex[..]);
191    let aabb_lines_and_triangles = None;
192    let spheres_raycast          = Some (&spheres_raycast_vertices[..]);
193    let meshes = {
194      let mut v = VecMap::with_capacity (MeshId::COUNT);
195      assert!(v.insert (MeshId::Grid       as usize, &grid_vertex_data[..]).is_none());
196      assert!(v.insert (MeshId::Hemisphere as usize, &hemisphere_vertex[..]).is_none());
197      assert!(v.insert (MeshId::Sphere     as usize, &sphere_vertex[..]).is_none());
198      assert!(v.insert (MeshId::Capsule    as usize, &capsule_vertex[..]).is_none());
199      assert!(v.insert (MeshId::Cube       as usize, &cube_vertex[..]).is_none());
200      assert!(v.insert (MeshId::Cylinder   as usize, &cylinder_vertex[..]).is_none());
201      v
202    };
203    // note: no billboard instances are created here, instead they are assigned
204    // to share instances with meshes
205    let billboards = VecMap::new();
206    self.resource.draw3d.instance_vertices_set (
207      &self.glium_display,
208      InstancesInit {
209        aabb_lines, aabb_triangles, aabb_lines_and_triangles, spheres_raycast, meshes,
210        billboards
211      }
212    );
213    // tile billboards
214    for mesh_id in MeshId::iter() {
215      let key = mesh_id as usize;
216      self.resource.draw3d.tile_billboard_create (
217        &self.glium_display, &self.resource.tileset_128x128_texture,
218        key, self.resource.draw3d.instanced_meshes()[key].instances_range.clone(),
219        format!("{mesh_id:?}").as_str()
220      );
221    }
222    let aabb_billboard_key = MeshId::COUNT;
223    self.resource.draw3d.tile_billboard_create (
224      &self.glium_display, &self.resource.tileset_128x128_texture,
225      aabb_billboard_key,  // start immediately after mesh billboards
226      self.resource.draw3d.instanced_aabb_lines().clone(),
227      "Aabb"
228    );
229
230    let (width, height) = self.window.inner_size().into();
231    let [_tile_width, tile_height] =
232      self.resource.tile_dimensions (DefaultTilesetId::EasciiAcorn128);
233    //
234    // 2D per-viewport tiles
235    //
236    let mut tile_2d_vertices = tile::vertices ("Viewport0", (0, 0));
237    let viewport0_tile_range = 0..tile_2d_vertices.len() as u32;
238    tile_2d_vertices.extend (tile::vertices ("Viewport1: +Y", (0, 0)));
239    let viewport1_tile_range = viewport0_tile_range.end..tile_2d_vertices.len()
240      as u32;
241    tile_2d_vertices.extend (tile::vertices ("Viewport2: +X", (0, 0)));
242    let viewport2_tile_range = viewport1_tile_range.end..tile_2d_vertices.len()
243      as u32;
244    tile_2d_vertices.extend (tile::vertices ("Viewport3: -Z", (0, 0)));
245    let viewport3_tile_range = viewport2_tile_range.end..tile_2d_vertices.len()
246      as u32;
247    //let center_tile = ((width / tile_width) / 2) as i32;
248    let fps_row = ((height / tile_height) - 1) as i32;
249    tile_2d_vertices.extend (tile::vertices ("FPS: 0     ", (fps_row, 0)));
250    let viewport4_tile_range = viewport3_tile_range.end..tile_2d_vertices.len()
251      as u32;
252    self.resource.draw2d.tile_2d_vertices = glium::VertexBuffer::dynamic (
253      &self.glium_display, &tile_2d_vertices[..]
254    ).unwrap();
255    let tile_color_2d_vertices = {
256      let tiles  = tile::vertices ("abc123",    (2, 0));
257      let colors = vec![
258        ( [1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0] ),
259        ( [0.0, 1.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0] ),
260        ( [0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0] ),
261        ( [0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 1.0] ),
262        ( [0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 1.0, 1.0] ),
263        ( [0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 1.0, 1.0] )
264      ];
265      tiles.into_iter().zip (colors).map (
266        |(vertex::Vert2dTile { tile, row, column }, (fg, bg))|
267        vertex::Vert2dTileColor { tile, row, column, fg, bg }
268      ).collect::<Vec <_>>()
269    };
270    let viewport0_tile_color_range = 0..tile_color_2d_vertices.len() as u32;
271    self.resource.draw2d.tile_color_2d_vertices = glium::VertexBuffer::dynamic (
272      &self.glium_display, &tile_color_2d_vertices[..]
273    ).unwrap();
274    let draw_tiles = draw2d::Tiles {
275      vertex_range: 0..0, origin: (2, 2).into(),
276      tileset_id: DefaultTilesetId::EasciiAcorn128
277    };
278    let draw_tiles_color = draw2d::Tiles {
279      vertex_range: 0..0, origin: draw2d::TilesOrigin::World,
280      tileset_id: DefaultTilesetId::EasciiAcorn256
281    };
282    self.resource.draw2d.viewport_resources_set (MAIN_VIEWPORT,
283      draw2d::ViewportResources {
284        draw_indices: vec![draw2d::DrawIndices {
285          rectangle:        None,
286          draw_tiles:       Some (draw2d::Tiles {
287            vertex_range: viewport0_tile_range,
288            .. draw_tiles.clone()
289          }),
290          draw_color_tiles: Some (draw2d::Tiles {
291            vertex_range: viewport0_tile_color_range,
292            .. draw_tiles_color
293          })
294        }],
295        .. draw2d::ViewportResources::default()
296      }
297    );
298    self.resource.draw2d.viewport_resources_set (UPPER_LEFT_VIEWPORT,
299      draw2d::ViewportResources {
300        draw_indices: vec![draw2d::DrawIndices {
301          draw_tiles: Some (draw2d::Tiles {
302            vertex_range: viewport1_tile_range,
303            .. draw_tiles.clone()
304          }),
305          .. draw2d::DrawIndices::default()
306        }],
307        .. draw2d::ViewportResources::default()
308      }
309    );
310    self.resource.draw2d.viewport_resources_set (UPPER_RIGHT_VIEWPORT,
311      draw2d::ViewportResources {
312        draw_indices: vec![draw2d::DrawIndices {
313          draw_tiles: Some (draw2d::Tiles {
314            vertex_range: viewport2_tile_range,
315            .. draw_tiles.clone()
316          }),
317          .. draw2d::DrawIndices::default()
318        }],
319        .. draw2d::ViewportResources::default()
320      }
321    );
322    self.resource.draw2d.viewport_resources_set (LOWER_LEFT_VIEWPORT,
323      draw2d::ViewportResources {
324        draw_indices: vec![draw2d::DrawIndices {
325          draw_tiles: Some (draw2d::Tiles {
326            vertex_range: viewport3_tile_range,
327            .. draw_tiles.clone()
328          }),
329          .. draw2d::DrawIndices::default()
330        }],
331        .. draw2d::ViewportResources::default()
332      }
333    );
334    self.resource.draw2d.viewport_resources_set (OVERLAY_VIEWPORT,
335      draw2d::ViewportResources {
336        draw_indices: vec![draw2d::DrawIndices {
337          draw_tiles: Some (draw2d::Tiles {
338            vertex_range: viewport4_tile_range,
339            .. draw_tiles
340          }),
341          .. draw2d::DrawIndices::default()
342        }],
343        draw_lineloop: false,
344        draw_crosshair: false
345      }
346    );
347    // create overlay viewport
348    let overlay = render::viewport::Builder::new (
349      glium::Rect { width, height, left: 0, bottom: 0 }
350    ).with_camera_3d (false).build();
351    assert!(self.viewports.insert (OVERLAY_VIEWPORT, overlay).is_none());
352  }
353
354  pub fn demo_handle_winit_window_event (&mut self,
355    event              : winit::event::WindowEvent,
356    running            : &mut bool,
357    mouse_position     : &mut (f64, f64),
358    mouse_button_event : &mut Option <winit::event::ElementState>
359  ) {
360    use std::f32::consts::PI;
361    use winit::{event, keyboard};
362    use num::Zero;
363    log::debug!("demo handle winit window event: {event:?}");
364    match event {
365      // window resized event
366      event::WindowEvent::Resized (physical_size) => {
367        log::debug!("window event: {event:?}");
368        self.window_resized (physical_size);
369      }
370      // window closed event
371      event::WindowEvent::CloseRequested => { *running = false; }
372      // TODO: for some reason a destroyed event is received on startup even though
373      // window has not been destroyed (winit 27.5)
374      #[expect(clippy::match_same_arms)]
375      event::WindowEvent::Destroyed => { /* *running = false; */ }
376      // modifiers changed event
377      event::WindowEvent::ModifiersChanged (modifiers) =>
378        *MODIFIERS.write().unwrap() = modifiers.state(),
379      // keyboard input pressed event
380      event::WindowEvent::KeyboardInput {
381        event: event::KeyEvent {
382          state: event::ElementState::Pressed,
383          physical_key, logical_key, location, ..
384        },
385        ..
386      } => match (logical_key, location) {
387        ( keyboard::Key::Named (keyboard::NamedKey::Control),
388          keyboard::KeyLocation::Left
389        ) =>
390          if Self::modifiers().shift_key() {
391            self.camera3d_move_local_xy (0.0, 0.0, -0.5)
392          } else {
393            self.camera3d_move_local_xy (0.0, 0.0, -1.0)
394          }
395        _ => match physical_key {
396          keyboard::PhysicalKey::Code (key_code) => match key_code {
397            // quit
398            keyboard::KeyCode::KeyQ | keyboard::KeyCode::Escape =>
399              *running = false,
400            // choose a frame function 0-9
401            keyboard::KeyCode::Digit1 =>
402              self.frame_fun = render::frame_fun_default::<Default>,
403            // TODO: more frame functions
404            // TODO: cycle between frame functions
405            //keyboard::KeyCode::Tab => { }
406            keyboard::KeyCode::KeyW =>
407              if Self::modifiers().shift_key() {
408                self.camera3d_move_local_xy (0.0, 0.5, 0.0)
409              } else {
410                self.camera3d_move_local_xy (0.0, 1.0, 0.0)
411              }
412            keyboard::KeyCode::KeyS =>
413              if Self::modifiers().shift_key() {
414                self.camera3d_move_local_xy (0.0, -0.5, 0.0)
415              } else {
416                self.camera3d_move_local_xy (0.0, -1.0, 0.0)
417              }
418            keyboard::KeyCode::KeyD =>
419              if Self::modifiers().shift_key() {
420                self.camera3d_move_local_xy (0.5, 0.0, 0.0)
421              } else {
422                self.camera3d_move_local_xy (1.0, 0.0, 0.0)
423              }
424            keyboard::KeyCode::KeyA =>
425              if Self::modifiers().shift_key() {
426                self.camera3d_move_local_xy (-0.5, 0.0, 0.0)
427              } else {
428                self.camera3d_move_local_xy (-1.0, 0.0, 0.0)
429              }
430            keyboard::KeyCode::Space | keyboard::KeyCode::KeyR =>
431              if Self::modifiers().shift_key() {
432                self.camera3d_move_local_xy (0.0, 0.0, 0.5)
433              } else {
434                self.camera3d_move_local_xy (0.0, 0.0, 1.0)
435              }
436            keyboard::KeyCode::KeyF =>
437              if Self::modifiers().shift_key() {
438                self.camera3d_move_local_xy (0.0, 0.0, -0.5)
439              } else {
440                self.camera3d_move_local_xy (0.0, 0.0, -1.0)
441              }
442            keyboard::KeyCode::KeyJ | keyboard::KeyCode::ArrowDown => {
443              let modifiers = Self::modifiers();
444              if modifiers.alt_key() && modifiers.shift_key() {
445                self.camera2d_move_local (0.0, -1.0);
446              } else {
447                if Self::modifiers().shift_key() {
448                  self.camera3d_rotate (
449                    math::Rad::zero(),
450                    math::Rad (-PI / 24.0),
451                    math::Rad::zero());
452                } else {
453                  self.camera3d_rotate (
454                    math::Rad::zero(),
455                    math::Rad (-PI / 12.0),
456                    math::Rad::zero());
457                }
458              }
459            }
460            keyboard::KeyCode::KeyK | keyboard::KeyCode::ArrowUp => {
461              let modifiers = Self::modifiers();
462              if modifiers.alt_key() && modifiers.shift_key() {
463                self.camera2d_move_local (0.0, 1.0);
464              } else {
465                if Self::modifiers().shift_key() {
466                  self.camera3d_rotate (
467                    math::Rad::zero(),
468                    math::Rad (PI / 24.0),
469                    math::Rad::zero());
470                } else {
471                  self.camera3d_rotate (
472                    math::Rad::zero(),
473                    math::Rad (PI / 12.0),
474                    math::Rad::zero());
475                }
476              }
477            }
478            keyboard::KeyCode::KeyH | keyboard::KeyCode::ArrowLeft => {
479              let modifiers = Self::modifiers();
480              if modifiers.alt_key() && modifiers.shift_key() {
481                self.camera2d_move_local (-1.0, 0.0);
482              } else {
483                if Self::modifiers().shift_key() {
484                  self.camera3d_rotate (
485                    math::Rad (PI / 24.0),
486                    math::Rad::zero(),
487                    math::Rad::zero());
488                } else {
489                  self.camera3d_rotate (
490                    math::Rad (PI / 12.0),
491                    math::Rad::zero(),
492                    math::Rad::zero());
493                }
494              }
495            }
496            keyboard::KeyCode::KeyL | keyboard::KeyCode::ArrowRight => {
497              let modifiers = Self::modifiers();
498              if modifiers.alt_key() && modifiers.shift_key() {
499                self.camera2d_move_local (1.0, 0.0);
500              } else {
501                if Self::modifiers().shift_key() {
502                  self.camera3d_rotate (
503                    math::Rad (-PI / 24.0),
504                    math::Rad::zero(),
505                    math::Rad::zero());
506                } else {
507                  self.camera3d_rotate (
508                    math::Rad (-PI / 12.0),
509                    math::Rad::zero(),
510                    math::Rad::zero());
511                }
512              }
513            }
514            keyboard::KeyCode::KeyI => {
515              let modifiers = Self::modifiers();
516              if modifiers.alt_key() && modifiers.shift_key() {
517                self.camera2d_zoom_shift (1.0);
518              } else if modifiers.alt_key() {
519                self.camera3d_orthographic_zoom_scale (1.1);
520              } else {
521                self.camera3d_perspective_fovy_scale (1.0 / 1.1);
522              }
523            }
524            keyboard::KeyCode::KeyO => {
525              let modifiers = Self::modifiers();
526              if modifiers.alt_key() && modifiers.shift_key() {
527                self.camera2d_zoom_shift (-1.0);
528              } else if modifiers.alt_key() {
529                self.camera3d_orthographic_zoom_scale (1.0 / 1.1);
530              } else {
531                self.camera3d_perspective_fovy_scale (1.1);
532              }
533            }
534            // toggle 4-viewport mode
535            keyboard::KeyCode::KeyX   => self.demo_toggle_quad_viewports(),
536            // end camera controls
537            keyboard::KeyCode::F10 | keyboard::KeyCode::PrintScreen =>
538              self.screenshot(),
539            _ => {}
540          }
541          keyboard::PhysicalKey::Unidentified (_) => {}
542        }
543      } // end keyboard input
544      event::WindowEvent::MouseInput { button: event::MouseButton::Left, state, .. } =>
545        *mouse_button_event = Some (state),
546      event::WindowEvent::CursorMoved { position, ..  } => {
547        if self.resource.draw2d.draw_pointer.is_some() {
548          let (_, height) = self.glium_display.get_framebuffer_dimensions();
549          let x = position.x as f32;
550          let y = height as f32 - position.y as f32;
551          self.resource
552            .set_pointer_position (&self.glium_display, [x, y].into());
553        }
554        *mouse_position = (position.x, position.y);
555      }
556      _ => {}
557    }
558  }
559
560  /// Switch between single (perspective) and quad viewport modes (perspective
561  /// + three ortho viewports).
562  ///
563  /// Generally should be called from a render context that was initialized
564  /// with `demo_init`.
565  ///
566  /// # Panics
567  ///
568  /// If there is a single viewport (other than the `OVERLAY_VIEWPORT`) it must
569  /// be `MAIN_VIEWPORT`.
570  ///
571  /// If there are multiple viewports, they must be exactly the first four
572  /// viewports only (other than the `OVERLAY_VIEWPORT`).
573  // TODO: doctests, make safe to call from any state?
574  fn demo_toggle_quad_viewports (&mut self) {
575    if self.viewports.len() <= 2 {
576      // switch to four viewports from single viewport
577      // main viewport becomes "lower right"
578      let (
579        left_width, right_width, upper_height, lower_height, position,
580        position_2d, zoom_2d
581      ) = {
582        let lower_right   = &mut self.viewports[MAIN_VIEWPORT];
583        let window_width  = lower_right.rect().width;
584        let window_height = lower_right.rect().height;
585        let left_width    = left_width   (window_width);
586        let right_width   = right_width  (window_width);
587        let upper_height  = upper_height (window_height);
588        let lower_height  = lower_height (window_height);
589        lower_right.set_rect (glium::Rect {
590          width:  right_width,
591          height: lower_height,
592          left:   left_width,
593          bottom: 0
594        });
595        ( left_width, right_width, upper_height, lower_height,
596          lower_right.camera3d().unwrap().position(),
597          lower_right.camera2d().unwrap().position(),
598          lower_right.camera2d().unwrap().zoom()
599        )
600      };
601
602      let upper_left  = render::viewport::Builder::new (glium::Rect {
603        width:  left_width,
604        height: upper_height,
605        left:   0,
606        bottom: lower_height
607      }).with_zoom_2d (zoom_2d).with_position_2d (position_2d)
608        .orthographic_3d (1.0)
609        // looking down positive Y axis
610        .with_pose_3d (math::Pose3 { position, angles: math::Angles3::default() })
611        .build();
612
613      let upper_right = render::viewport::Builder::new (glium::Rect {
614        width:  right_width,
615        height: upper_height,
616        left:   left_width,
617        bottom: lower_height,
618      }).with_zoom_2d (zoom_2d).with_position_2d (position_2d)
619        .orthographic_3d (1.0)
620        .with_pose_3d (
621          math::Pose3 {
622            position,
623            // looking down positive X axis
624            angles: math::Angles3::wrap (
625              math::Deg (-90.0).into(),
626              math::Rad (0.0),
627              math::Rad (0.0))
628          })
629        .build();
630
631      let lower_left  = render::viewport::Builder::new (glium::Rect {
632        width:  left_width,
633        height: lower_height,
634        left:   0,
635        bottom: 0,
636      }).with_zoom_2d (zoom_2d).with_position_2d (position_2d)
637        .orthographic_3d (1.0)
638        .with_pose_3d (
639          math::Pose3 {
640            position,
641            // looking down negative Z axis
642            angles: math::Angles3::wrap (
643              math::Rad (0.0),
644              math::Deg (-90.0).into(),
645              math::Rad (0.0))
646          })
647        .build();
648
649      assert!(self.viewports.insert (UPPER_LEFT_VIEWPORT, upper_left).is_none());
650      assert!(self.viewports.insert (UPPER_RIGHT_VIEWPORT, upper_right).is_none());
651      assert!(self.viewports.insert (LOWER_LEFT_VIEWPORT, lower_left).is_none());
652    } else {
653      // switch to single viewport from four viewports
654      assert!(self.viewports.len() == 4 || self.viewports.len() == 5);
655      assert!(self.viewports.remove (UPPER_LEFT_VIEWPORT) .is_some());
656      assert!(self.viewports.remove (UPPER_RIGHT_VIEWPORT).is_some());
657      assert!(self.viewports.remove (LOWER_LEFT_VIEWPORT) .is_some());
658      let main_viewport   = &mut self.viewports[MAIN_VIEWPORT];
659      let (width, height) = self.window.inner_size().into();
660      main_viewport.set_rect (glium::Rect {
661        width,
662        height,
663        left:   0,
664        bottom: 0
665      });
666    }
667    self.update_viewport_line_loops();
668  } // end fn toggle_quad_viewports
669}