lib.rs
1 #![deny(clippy::all)] 2 #![allow( 3 clippy::collapsible_if, 4 clippy::many_single_char_names, 5 clippy::expect_fun_call, 6 clippy::useless_format, 7 clippy::new_without_default, 8 clippy::cognitive_complexity, 9 clippy::comparison_chain, 10 clippy::type_complexity, 11 clippy::or_fun_call, 12 clippy::nonminimal_bool, 13 clippy::single_match, 14 clippy::large_enum_variant 15 )] 16 17 pub mod data; 18 pub mod execution; 19 pub mod gfx; 20 pub mod logger; 21 pub mod session; 22 23 mod alloc; 24 mod autocomplete; 25 mod brush; 26 mod cmd; 27 mod color; 28 mod draw; 29 mod event; 30 mod flood; 31 mod font; 32 mod gl; 33 mod history; 34 mod image; 35 mod io; 36 mod palette; 37 mod parser; 38 mod pixels; 39 mod platform; 40 mod renderer; 41 mod sprite; 42 mod timer; 43 mod view; 44 45 #[macro_use] 46 pub mod util; 47 48 use cmd::Value; 49 use event::Event; 50 use execution::{DigestMode, Execution, ExecutionMode}; 51 use platform::{WindowEvent, WindowHint}; 52 use renderer::Renderer; 53 use session::*; 54 use timer::FrameTimer; 55 use view::FileStatus; 56 57 #[macro_use] 58 extern crate log; 59 60 use directories as dirs; 61 62 use std::alloc::System; 63 use std::path::{Path, PathBuf}; 64 use std::time::{Duration, Instant}; 65 66 /// Program version. 67 pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 68 69 #[global_allocator] 70 pub static ALLOCATOR: alloc::Allocator = alloc::Allocator::new(System); 71 72 #[derive(Debug)] 73 pub struct Options<'a> { 74 pub width: u32, 75 pub height: u32, 76 pub resizable: bool, 77 pub headless: bool, 78 pub source: Option<PathBuf>, 79 pub exec: ExecutionMode, 80 pub glyphs: &'a [u8], 81 pub debug: bool, 82 } 83 84 impl<'a> Default for Options<'a> { 85 fn default() -> Self { 86 Self { 87 width: 1280, 88 height: 720, 89 headless: false, 90 resizable: true, 91 source: None, 92 exec: ExecutionMode::Normal, 93 glyphs: data::GLYPHS, 94 debug: false, 95 } 96 } 97 } 98 99 pub fn init<P: AsRef<Path>>(paths: &[P], options: Options<'_>) -> std::io::Result<()> { 100 use std::io; 101 102 debug!("options: {:?}", options); 103 104 let hints = &[ 105 WindowHint::Resizable(options.resizable), 106 WindowHint::Visible(!options.headless), 107 ]; 108 let (mut win, mut events) = platform::init( 109 "rx", 110 options.width, 111 options.height, 112 hints, 113 platform::GraphicsContext::Gl, 114 )?; 115 116 let scale_factor = win.scale_factor(); 117 let win_size = win.size(); 118 let (win_w, win_h) = (win_size.width as u32, win_size.height as u32); 119 120 info!("framebuffer size: {}x{}", win_size.width, win_size.height); 121 info!("scale factor: {}", scale_factor); 122 123 let assets = data::Assets::new(options.glyphs); 124 let proj_dirs = dirs::ProjectDirs::from("io", "cloudhead", "rx") 125 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "config directory not found"))?; 126 let base_dirs = dirs::BaseDirs::new() 127 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "home directory not found"))?; 128 let cwd = std::env::current_dir()?; 129 let mut session = Session::new(win_w, win_h, cwd, proj_dirs, base_dirs) 130 .with_blank( 131 FileStatus::NoFile, 132 Session::DEFAULT_VIEW_W, 133 Session::DEFAULT_VIEW_H, 134 ) 135 .init(options.source.clone())?; 136 137 if options.debug { 138 session 139 .settings 140 .set("debug", Value::Bool(true)) 141 .expect("'debug' is a bool'"); 142 } 143 144 let mut execution = match options.exec { 145 ExecutionMode::Normal => Execution::normal(), 146 ExecutionMode::Replay(path, digest) => Execution::replaying(path, digest), 147 ExecutionMode::Record(path, digest, gif) => { 148 Execution::recording(path, digest, win_w as u16, win_h as u16, gif) 149 } 150 }?; 151 152 // When working with digests, certain settings need to be overwritten 153 // to ensure things work correctly. 154 match &execution { 155 Execution::Replaying { digest, .. } | Execution::Recording { digest, .. } 156 if digest.mode != DigestMode::Ignore => 157 { 158 session 159 .settings 160 .set("animation", Value::Bool(false)) 161 .expect("'animation' is a bool"); 162 } 163 _ => {} 164 } 165 166 let wait_events = execution.is_normal() || execution.is_recording(); 167 168 let mut renderer: gl::Renderer = Renderer::new(&mut win, win_size, scale_factor, assets)?; 169 170 if let Err(e) = session.edit(paths) { 171 session.message(format!("Error loading path(s): {}", e), MessageType::Error); 172 } 173 // Make sure our session ticks once before anything is rendered. 174 let effects = session.update( 175 &mut vec![], 176 &mut execution, 177 Duration::default(), 178 Duration::default(), 179 ); 180 renderer.init(effects, &session); 181 182 let mut render_timer = FrameTimer::new(); 183 let mut update_timer = FrameTimer::new(); 184 let mut session_events = Vec::with_capacity(16); 185 let mut last = Instant::now(); 186 let mut resized = false; 187 let mut hovering = false; 188 let mut delta; 189 190 while !win.is_closing() { 191 match session.animation_delay() { 192 Some(delay) if session.is_running() => { 193 // How much time is left until the next animation frame? 194 let remaining = delay - session.accumulator; 195 // If more than 1ms remains, let's wait. 196 if remaining.as_millis() > 1 { 197 events.wait_timeout(remaining); 198 } else { 199 events.poll(); 200 } 201 } 202 _ if wait_events => events.wait(), 203 _ => events.poll(), 204 } 205 206 for event in events.flush() { 207 if event.is_input() { 208 debug!("event: {:?}", event); 209 } 210 211 match event { 212 WindowEvent::Resized(size) => { 213 if size.is_zero() { 214 // On certain operating systems, the window size will be set to 215 // zero when the window is minimized. Since a zero-sized framebuffer 216 // is not valid, we pause the session until the window is restored. 217 session.transition(State::Paused); 218 } else { 219 resized = true; 220 session.transition(State::Running); 221 } 222 } 223 WindowEvent::CursorEntered { .. } => { 224 if win.is_focused() { 225 win.set_cursor_visible(false); 226 } 227 hovering = true; 228 } 229 WindowEvent::CursorLeft { .. } => { 230 win.set_cursor_visible(true); 231 232 hovering = false; 233 } 234 WindowEvent::Minimized => { 235 session.transition(State::Paused); 236 } 237 WindowEvent::Restored => { 238 if win.is_focused() { 239 session.transition(State::Running); 240 } 241 } 242 WindowEvent::Focused(true) => { 243 session.transition(State::Running); 244 245 if hovering { 246 win.set_cursor_visible(false); 247 } 248 } 249 WindowEvent::Focused(false) => { 250 win.set_cursor_visible(true); 251 session.transition(State::Paused); 252 } 253 WindowEvent::RedrawRequested => { 254 render_timer.run(|avg| { 255 renderer 256 .frame(&mut session, &mut execution, vec![], &avg) 257 .unwrap_or_else(|err| { 258 log::error!("{}", err); 259 }); 260 }); 261 win.present(); 262 } 263 WindowEvent::ScaleFactorChanged(factor) => { 264 renderer.handle_scale_factor_changed(factor); 265 } 266 WindowEvent::CloseRequested => { 267 session.quit(ExitReason::Normal); 268 } 269 WindowEvent::CursorMoved { position } => { 270 session_events.push(Event::CursorMoved(position)); 271 } 272 WindowEvent::MouseInput { state, button, .. } => { 273 session_events.push(Event::MouseInput(button, state)); 274 } 275 WindowEvent::MouseWheel { delta, .. } => { 276 session_events.push(Event::MouseWheel(delta)); 277 } 278 WindowEvent::KeyboardInput(input) => match input { 279 // Intercept `<insert>` key for pasting. 280 // 281 // Reading from the clipboard causes the loop to wake up for some strange 282 // reason I cannot comprehend. So we only read from clipboard when we 283 // need to paste. 284 platform::KeyboardInput { 285 key: Some(platform::Key::Insert), 286 state: platform::InputState::Pressed, 287 modifiers: platform::ModifiersState { shift: true, .. }, 288 } => { 289 session_events.push(Event::Paste(win.clipboard())); 290 } 291 _ => session_events.push(Event::KeyboardInput(input)), 292 }, 293 WindowEvent::ReceivedCharacter(c, mods) => { 294 session_events.push(Event::ReceivedCharacter(c, mods)); 295 } 296 _ => {} 297 }; 298 } 299 300 if resized { 301 // Instead of responded to each resize event by creating a new framebuffer, 302 // we respond to the event *once*, here. 303 resized = false; 304 session.handle_resized(win.size()); 305 } 306 307 delta = last.elapsed(); 308 last += delta; 309 310 // If we're paused, we want to keep the timer running to not get a 311 // "jump" when we unpause, but skip session updates and rendering. 312 if session.state == State::Paused { 313 continue; 314 } 315 316 let effects = 317 update_timer.run(|avg| session.update(&mut session_events, &mut execution, delta, avg)); 318 319 render_timer.run(|avg| { 320 renderer 321 .frame(&mut session, &mut execution, effects, &avg) 322 .unwrap_or_else(|err| { 323 log::error!("{}", err); 324 }); 325 }); 326 327 session.cleanup(); 328 win.present(); 329 330 match session.state { 331 State::Closing(ExitReason::Normal) => { 332 return Ok(()); 333 } 334 State::Closing(ExitReason::Error(e)) => { 335 return Err(io::Error::new(io::ErrorKind::Other, e)); 336 } 337 _ => {} 338 } 339 } 340 341 Ok(()) 342 }