diff --git a/.gitignore b/.gitignore index 05c70e9c58..1c6cfbe9a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ Cargo.lock target/ rls/ +# pkg: Often used for temporary wasm debug builds +pkg/ .vscode/ *~ #*# diff --git a/Cargo.toml b/Cargo.toml index 7385cf7e46..78ebf5a79d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,7 @@ smol_str = "0.3" tracing = { version = "0.1.40", default-features = false } [dev-dependencies] +font8x8 = "0.3.1" image = { version = "0.25.0", default-features = false, features = ["png"] } tracing = { version = "0.1.40", default-features = false, features = ["log"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } @@ -312,6 +313,10 @@ xkbcommon-dl = "0.4.2" orbclient = { version = "0.3.47", default-features = false } redox_syscall = "0.5.7" +# We want the web-time crate on non-web clients too, for examples (hence dev-dependencies) +[target.'cfg(not(target_family = "wasm"))'.dev-dependencies] +web-time = "1" + # Web [target.'cfg(target_family = "wasm")'.dependencies] js-sys = "0.3.70" diff --git a/examples/tester.html b/examples/tester.html new file mode 100644 index 0000000000..86c7bb5eb1 --- /dev/null +++ b/examples/tester.html @@ -0,0 +1,17 @@ + + + + + + Winit Tablet Tester + + + + + + \ No newline at end of file diff --git a/examples/tester.rs b/examples/tester.rs new file mode 100644 index 0000000000..8c594ee9af --- /dev/null +++ b/examples/tester.rs @@ -0,0 +1,313 @@ +//! Basic winit interactivity example. + +//! Web compilation and testing example: +//! cargo build --release --target wasm32-unknown-unknown --example tester && \ +//! wasm-bindgen target/wasm32-unknown-unknown/release/examples/tester.wasm \ +//! --out-dir pkg --target web +//! python3 -m http.server +//! Then navigate to http://localhost:8000/examples/tester.html + +use std::error::Error; + +use font8x8::legacy::BASIC_LEGACY; +use winit::application::ApplicationHandler; +use winit::event::{ButtonSource, MouseButton, PointerSource, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +#[cfg(web_platform)] +use winit::platform::web::WindowAttributesExtWeb; +use winit::window::{Window, WindowAttributes, WindowId}; + +fn draw_char(frame: &mut [u32], width: usize, x: u32, y: u32, ch: char, fg: u32, bg: u32) { + let x: usize = x.try_into().unwrap(); + let y: usize = y.try_into().unwrap(); + let glyph = BASIC_LEGACY.get(ch as usize).unwrap_or(&BASIC_LEGACY[' ' as usize]); + for (row, byte) in glyph.iter().enumerate() { + let ypart = (y + row) * width; + for col in 0..8 { + let i = ypart + (x + col); + if i < frame.len() && byte & (1 << col) != 0 { + frame[i] = fg; + } else { + frame[i] = bg; + } + } + } +} + +fn draw_text( + frame: &mut [u32], + width: usize, + mut x: u32, + mut y: u32, + text: &str, + fg: u32, + bg: u32, +) -> (u32, u32) { + let x_init = x; + let mut max_x = x; + let mut max_y = y; + for ch in text.chars() { + if ch == '\n' { + x = x_init; + y += 8; + max_y = max_y.max(y); + } else { + draw_char(frame, width, x, y, ch, fg, bg); + x += 8; + max_x = max_x.max(x); + } + } + (max_x + 8, max_y + 8) +} + +#[path = "util/fill.rs"] +mod fill; +#[path = "util/tracing.rs"] +mod tracing; + +#[derive(Default, Debug)] +struct App { + window: Option>, + old_posx: f32, + old_posy: f32, + posx: f32, + posy: f32, + has_pressure: Option, + drawing: bool, + last_draw: Vec<[u32; 4]>, +} + +impl ApplicationHandler for App { + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + #[cfg(not(web_platform))] + let window_attributes = WindowAttributes::default(); + #[cfg(web_platform)] + let window_attributes = WindowAttributes::default().with_append(true); + self.window = match event_loop.create_window(window_attributes) { + Ok(window) => Some(window), + Err(err) => { + eprintln!("error creating window: {err}"); + event_loop.exit(); + return; + }, + }; + + let window = self.window.as_ref().unwrap(); + window.pre_present_notify(); + fill::fill_window_with_color(&**window, 0xff181818); + window.request_redraw(); + } + + fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => { + println!("Close was requested; stopping"); + event_loop.exit(); + }, + WindowEvent::SurfaceResized(_) => { + let window = self.window.as_ref().expect("resize event without a window"); + window.pre_present_notify(); + fill::fill_window_with_color(&**window, 0xff181818); + window.request_redraw(); + }, + WindowEvent::PointerButton { primary, position, state, button, .. } => { + let window = self.window.as_ref().expect("resize event without a window"); + let scale = window.scale_factor(); + match (primary, button) { + (true, ButtonSource::Mouse(MouseButton::Left)) => { + self.posx = position.to_logical::(scale).x; + self.posy = position.to_logical::(scale).y; + self.old_posx = position.to_logical::(scale).x; + self.old_posy = position.to_logical::(scale).y; + self.drawing = state == winit::event::ElementState::Pressed; + self.has_pressure = None; + }, + (true, ButtonSource::Touch { force, .. }) => { + self.posx = position.to_logical::(scale).x; + self.posy = position.to_logical::(scale).y; + self.old_posx = position.to_logical::(scale).x; + self.old_posy = position.to_logical::(scale).y; + self.drawing = state == winit::event::ElementState::Pressed; + self.has_pressure = force.map(|x| x.normalized() as f32); + }, + _ => { + let window = self.window.as_ref().unwrap(); + window.pre_present_notify(); + fill::fill_window_with_color(&**window, 0xff181818); + window.request_redraw(); + }, + } + }, + WindowEvent::PointerMoved { position, source, .. } => { + let window = self.window.as_ref().expect("resize event without a window"); + let scale = window.scale_factor(); + if self.drawing { + self.posx = position.to_logical::(scale).x; + self.posy = position.to_logical::(scale).y; + } + if let PointerSource::Touch { force, .. } = source { + if self.has_pressure.is_some() { + self.has_pressure = force.map(|x| x.normalized() as f32); + } + } + }, + WindowEvent::RedrawRequested => { + // Redraw the application. + // + // It's preferable for applications that do not render continuously to render in + // this event rather than in AboutToWait, since rendering in here allows + // the program to gracefully handle redraws requested by the OS. + + let window = self.window.as_ref().expect("redraw request without a window"); + + // Notify that you're about to draw. + window.pre_present_notify(); + + let mut rects = vec![]; + let rects_ref = &mut rects; + // Draw. + fill::fill_window_with_fn(&**window, |frame, stride, scale, frame_w, frame_h| { + // Clear the top left 50x50 rect, we'll put an animation there. + for y in 0..((50.0 * scale) as usize).min(frame_h as usize) { + for x in 0..((50.0 * scale) as usize).min(frame_w as usize) { + frame[y * stride + x] = 0xff181818; + } + } + + let extent = draw_text( + frame, + stride, + (20.0 * scale) as u32, + (50.0 * scale) as u32, + &format!( + "Input tester.\nLeft click to draw, right click to clear.\nx: {}\ny: \ + {}\npressure: {:?}", + self.posx, self.posy, self.has_pressure + ), + 0xffffffff, + 0xff181818, + ); + let rect1 = [20, 50, extent.0 - 20, extent.1 - 50]; + + let mut draw_line = + |xpos: f32, ypos: f32, xoff: f32, yoff: f32, thickness: u32| { + if xoff == 0.0 && yoff == 0.0 { + return; + } + let len = (xoff * xoff + yoff * yoff).sqrt(); + let norm = 1.0 / xoff.abs().max(yoff.abs()); + let xo_small = xoff * norm; + let yo_small = yoff * norm; + let spacing = (thickness as f32 * 0.25).max(1.0); + let antinorm = + 1.0 / (xo_small * xo_small + yo_small * yo_small).sqrt() / spacing; + for i in 0..=(len * antinorm) as usize { + let i = i as f32; + + for _y in -(thickness as i32) / 2..(thickness as i32 + 1) / 2 { + for _x in -(thickness as i32) / 2..(thickness as i32 + 1) / 2 { + let x = (xpos + _x as f32 + xo_small * i * spacing) + * scale as f32; + let y = (ypos + _y as f32 + yo_small * i * spacing) + * scale as f32; + + let xpart = x.clamp(0.0, frame_w as f32 - 1.0) as usize; + let ypart = + y.clamp(0.0, frame_h as f32 - 1.0) as usize * stride; + frame[ypart + xpart] = 0xffffffff; + } + } + } + }; + + // Animation. + let x = 25.0; + let y = 25.0; + + static START: std::sync::OnceLock = + std::sync::OnceLock::new(); + let start = START.get_or_init(web_time::Instant::now); + let time = web_time::Instant::now().duration_since(*start).as_secs_f32(); + + let xo = (time).sin() * 25.0; + let yo = (time).cos() * 25.0; + draw_line(x, y, xo, yo, 1); + draw_line(x, y, -xo, -yo, 1); + + // Any new lines to draw from input? + let diameter = self.has_pressure.map(|x| x * 10.0).unwrap_or(0.0).ceil(); + if self.drawing { + draw_line( + self.old_posx, + self.old_posy, + self.posx - self.old_posx, + self.posy - self.old_posy, + diameter as u32 + 1, + ); + } + let radius = (diameter * 0.5).ceil(); + + let x = (self.posx.min(self.old_posx) - radius).max(0.0); + let y = (self.posy.min(self.old_posy) - radius).max(0.0); + let w = + (self.posx.max(self.old_posx) - x + 1.0 + radius).min(frame_w as f32 - x); + let h = + (self.posy.max(self.old_posy) - y + 1.0 + radius).min(frame_h as f32 - y); + + *rects_ref = + vec![[0, 0, 50, 50], rect1, [x as u32, y as u32, w as u32, h as u32]]; + let mut damaged = rects_ref.clone(); + damaged.extend_from_slice(&self.last_draw); + damaged + }); + + self.old_posx = self.posx; + self.old_posy = self.posy; + + self.last_draw = rects; + + // For contiguous redraw loop you can request a redraw from here. + window.request_redraw(); + // Don't run hundreds of thousands of times per second even if the platform asks. + // (Can't sleep on web, so gated off there. Browsers won't ask for too much.) + #[cfg(not(web_platform))] + { + std::thread::sleep(std::time::Duration::from_millis(1)); + } + }, + _ => (), + } + } +} + +fn main() -> Result<(), Box> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + let event_loop = EventLoop::new()?; + + // For alternative loop run options see `pump_events` and `run_on_demand` examples. + event_loop.run_app(App::default())?; + + Ok(()) +} + +#[cfg(web_platform)] +use wasm_bindgen::prelude::wasm_bindgen; +#[cfg(web_platform)] +#[wasm_bindgen(start)] +pub fn start() -> Result<(), wasm_bindgen::JsValue> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + let event_loop = EventLoop::new().unwrap(); + + // For alternative loop run options see `pump_events` and `run_on_demand` examples. + event_loop.run_app(App::default()).unwrap(); + + Ok(()) +} diff --git a/examples/util/fill.rs b/examples/util/fill.rs index f7a6a4e2f0..e3e7fd56d5 100644 --- a/examples/util/fill.rs +++ b/examples/util/fill.rs @@ -15,6 +15,8 @@ pub use platform::fill_window; pub use platform::fill_window_with_animated_color; #[allow(unused_imports)] pub use platform::fill_window_with_color; +#[allow(unused_imports)] +pub use platform::fill_window_with_fn; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod platform { @@ -75,6 +77,50 @@ mod platform { } } + #[allow(dead_code)] + pub fn fill_window_with_fn( + window: &dyn Window, + f: impl FnOnce(&mut [u32], usize, f32, u32, u32) -> Vec<[u32; 4]>, + ) { + GC.with(|gc| { + let size = window.surface_size(); + let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + else { + return; + }; + + // Either get the last context used or create a new one. + let mut gc = gc.borrow_mut(); + let surface = + gc.get_or_insert_with(|| GraphicsContext::new(window)).create_surface(window); + + surface.resize(width, height).expect("Failed to resize the softbuffer surface"); + + // Run a function on the buffer. + let mut buffer = surface.buffer_mut().expect("Failed to get the softbuffer buffer"); + let rects = f( + &mut buffer, + u32::from(width) as usize, + window.scale_factor() as f32, + width.into(), + height.into(), + ); + // Collect only valid rects, and as softbuffer's expected type. + let rects = rects + .iter() + .filter(|r| r[2] != 0 && r[3] != 0) + .map(|r| softbuffer::Rect { + x: r[0], + y: r[1], + width: r[2].try_into().unwrap(), + height: r[3].try_into().unwrap(), + }) + .collect::>(); + buffer.present_with_damage(&rects).expect("Failed to present the softbuffer buffer"); + }) + } + pub fn fill_window_with_color(window: &dyn Window, color: u32) { GC.with(|gc| { let size = window.surface_size(); @@ -137,6 +183,14 @@ mod platform { // No-op on mobile platforms. } + #[allow(dead_code)] + pub fn fill_window_with_fn( + _window: &dyn winit::window::Window, + _f: impl FnOnce(&mut [u32], usize, f64, u32, u32) -> Vec<[u32; 4]>, + ) { + // No-op on mobile platforms. + } + #[allow(dead_code)] pub fn fill_window_with_animated_color( _window: &dyn winit::window::Window,