├── .github ├── FUNDING.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── hello_world.rs ├── render_test.rs └── simple_demo.rs ├── screenshot.png └── src ├── lib.rs ├── renderer.rs ├── renderer ├── opengl.rs ├── opengl │ └── renderer.rs ├── wgpu.rs └── wgpu │ └── renderer.rs ├── translate.rs └── window.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: BillyDM 4 | custom: ["paypal.me/BillyDMdeveloper"] 5 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macOS-latest] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install XCB and GL dependencies 15 | run: | 16 | sudo apt update 17 | # baseview dependencies 18 | sudo apt install libx11-xcb-dev libxcb-dri2-0-dev libgl1-mesa-dev libxcb-icccm4-dev libxcursor-dev libxkbcommon-dev libxcb-shape0-dev libxcb-xfixes0-dev 19 | if: contains(matrix.os, 'ubuntu') 20 | - name: Install rust stable 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | override: true 25 | - name: Build 26 | run: cargo build --verbose 27 | - name: Run tests 28 | run: cargo test --verbose 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | # 15 | # already existing elements were commented out 16 | 17 | /target 18 | #Cargo.lock 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "egui-baseview" 3 | version = "0.5.0" 4 | authors = ["Billy Messenger <60663878+BillyDM@users.noreply.github.com>"] 5 | edition = "2021" 6 | description = "A baseview backend for egui" 7 | license = "MIT" 8 | repository = "https://github.com/BillyDM/egui-baseview" 9 | documentation = "https://docs.rs/egui-baseview" 10 | keywords = ["gui", "ui", "graphics", "interface", "widgets"] 11 | categories = ["gui"] 12 | readme = "README.md" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [features] 17 | default = ["opengl", "default_fonts"] 18 | default_fonts = ["egui/default_fonts"] 19 | opengl = ["dep:egui_glow", "baseview/opengl"] 20 | wgpu = ["dep:egui-wgpu", "dep:raw-window-handle-06", "dep:pollster", "dep:wgpu"] 21 | ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). 22 | ## 23 | ## This can help performance for graphics-intense applications. 24 | rayon = ["egui/rayon"] 25 | ## Enables a temporary workaround for keyboard input not working sometimes in Windows. 26 | ## See https://github.com/BillyDM/egui-baseview/issues/20 27 | windows_keyboard_workaround = [] 28 | 29 | [dependencies] 30 | egui = { version = "0.31", default-features = false, features = ["bytemuck"] } 31 | egui_glow = { version = "0.31", features = ["x11"], optional = true } 32 | egui-wgpu = { version = "0.31", features = ["x11"], optional = true } 33 | wgpu = { version = "24.0.0", optional = true } 34 | keyboard-types = { version = "0.6", default-features = false } 35 | baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "9a0b42c09d712777b2edb4c5e0cb6baf21e988f0" } 36 | raw-window-handle = "0.5" 37 | raw-window-handle-06 = { package = "raw-window-handle", version = "0.6", optional = true } 38 | # TODO: Enable wayland feature when baseview gets wayland support. 39 | copypasta = { version = "0.10", default-features = false, features = ["x11"] } 40 | log = "0.4" 41 | open = "5.1" 42 | pollster = { version = "0.4", optional = true } 43 | thiserror = "2.0" 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Billy Messenger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egui-baseview 2 | ![Test](https://github.com/BillyDM/egui-baseview/workflows/Rust/badge.svg) 3 | [![License](https://img.shields.io/crates/l/egui-baseview.svg)](https://github.com/BillyDM/egui-baseview/blob/main/LICENSE) 4 | 5 | A [`baseview`](https://github.com/RustAudio/baseview) backend for [`egui`](https://github.com/emilk/egui). 6 | 7 |
8 | 9 |
10 | 11 | ## Audio Plugins 12 | 13 | This backend is officially supported by [`nih-plug`](https://github.com/robbert-vdh/nih-plug). The nih-plug example plugin can be found [here](https://github.com/robbert-vdh/nih-plug/tree/master/plugins/examples/gain_gui_egui). 14 | 15 | There is also an (outdated) vst2 example plugin [here](https://github.com/DGriffin91/egui_baseview_test_vst2). 16 | 17 | ## Prerequisites 18 | 19 | ### Linux 20 | 21 | Install dependencies, e.g., 22 | 23 | ```sh 24 | sudo apt-get install libx11-dev libxcursor-dev libxcb-dri2-0-dev libxcb-icccm4-dev libx11-xcb-dev mesa-common-dev libgl1-mesa-dev libglu1-mesa-dev 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use baseview::{Size, WindowOpenOptions, WindowScalePolicy}; 2 | use egui::Context; 3 | use egui_baseview::{EguiWindow, GraphicsConfig, Queue}; 4 | 5 | fn main() { 6 | let settings = WindowOpenOptions { 7 | title: String::from("egui-baseview hello world"), 8 | size: Size::new(300.0, 110.0), 9 | scale: WindowScalePolicy::SystemScaleFactor, 10 | #[cfg(feature = "opengl")] 11 | gl_config: Some(Default::default()), 12 | }; 13 | 14 | let state = (); 15 | 16 | EguiWindow::open_blocking( 17 | settings, 18 | GraphicsConfig::default(), 19 | state, 20 | |_egui_ctx: &Context, _queue: &mut Queue, _state: &mut ()| {}, 21 | |egui_ctx: &Context, _queue: &mut Queue, _state: &mut ()| { 22 | egui::Window::new("egui-baseview hello world").show(egui_ctx, |ui| { 23 | ui.label("Hello World!"); 24 | }); 25 | }, 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/render_test.rs: -------------------------------------------------------------------------------- 1 | use baseview::{Size, WindowOpenOptions, WindowScalePolicy}; 2 | use egui::Context; 3 | use egui_baseview::{EguiWindow, GraphicsConfig, Queue}; 4 | 5 | use std::collections::HashMap; 6 | 7 | use egui::{widgets::color_picker::show_color, TextureOptions, *}; 8 | 9 | const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0); 10 | 11 | const BLACK: Color32 = Color32::BLACK; 12 | const GREEN: Color32 = Color32::GREEN; 13 | const RED: Color32 = Color32::RED; 14 | const TRANSPARENT: Color32 = Color32::TRANSPARENT; 15 | const WHITE: Color32 = Color32::WHITE; 16 | 17 | /// A test for sanity-checking and diagnosing egui rendering backends. 18 | pub struct ColorTest { 19 | tex_mngr: TextureManager, 20 | vertex_gradients: bool, 21 | texture_gradients: bool, 22 | } 23 | 24 | impl Default for ColorTest { 25 | fn default() -> Self { 26 | Self { 27 | tex_mngr: Default::default(), 28 | vertex_gradients: true, 29 | texture_gradients: true, 30 | } 31 | } 32 | } 33 | 34 | impl ColorTest { 35 | pub fn ui(&mut self, ui: &mut Ui) { 36 | ui.horizontal_wrapped(|ui|{ 37 | ui.label("This is made to test that the egui rendering backend is set up correctly."); 38 | ui.add(egui::Label::new("❓").sense(egui::Sense::click())) 39 | .on_hover_text("The texture sampling should be sRGB-aware, and every other color operation should be done in gamma-space (sRGB). All colors should use pre-multiplied alpha"); 40 | }); 41 | 42 | ui.separator(); 43 | 44 | pixel_test(ui); 45 | 46 | ui.separator(); 47 | 48 | ui.collapsing("Color test", |ui| { 49 | self.color_test(ui); 50 | }); 51 | 52 | ui.separator(); 53 | 54 | ui.heading("Text rendering"); 55 | 56 | text_on_bg(ui, Color32::from_gray(200), Color32::from_gray(230)); // gray on gray 57 | text_on_bg(ui, Color32::from_gray(140), Color32::from_gray(28)); // dark mode normal text 58 | 59 | // Matches Mac Font book (useful for testing): 60 | text_on_bg(ui, Color32::from_gray(39), Color32::from_gray(255)); 61 | text_on_bg(ui, Color32::from_gray(220), Color32::from_gray(30)); 62 | 63 | ui.separator(); 64 | 65 | blending_and_feathering_test(ui); 66 | } 67 | 68 | fn color_test(&mut self, ui: &mut Ui) { 69 | ui.label("If the rendering is done right, all groups of gradients will look uniform."); 70 | 71 | ui.horizontal(|ui| { 72 | ui.checkbox(&mut self.vertex_gradients, "Vertex gradients"); 73 | ui.checkbox(&mut self.texture_gradients, "Texture gradients"); 74 | }); 75 | 76 | ui.heading("sRGB color test"); 77 | ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500"); 78 | ui.scope(|ui| { 79 | ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients 80 | let g = Gradient::one_color(Color32::from_rgb(255, 165, 0)); 81 | self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g); 82 | self.tex_gradient(ui, "orange rgb(255, 165, 0) - texture", WHITE, &g); 83 | }); 84 | 85 | ui.separator(); 86 | 87 | ui.label("Test that vertex color times texture color is done in gamma space:"); 88 | ui.scope(|ui| { 89 | ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients 90 | 91 | let tex_color = Color32::from_rgb(64, 128, 255); 92 | let vertex_color = Color32::from_rgb(128, 196, 196); 93 | let ground_truth = mul_color_gamma(tex_color, vertex_color); 94 | 95 | ui.horizontal(|ui| { 96 | let color_size = ui.spacing().interact_size; 97 | ui.label("texture"); 98 | show_color(ui, tex_color, color_size); 99 | ui.label(" * "); 100 | show_color(ui, vertex_color, color_size); 101 | ui.label(" vertex color ="); 102 | }); 103 | { 104 | let g = Gradient::one_color(ground_truth); 105 | self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g); 106 | self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g); 107 | } 108 | 109 | ui.horizontal(|ui| { 110 | let g = Gradient::one_color(tex_color); 111 | let tex = self.tex_mngr.get(ui.ctx(), &g); 112 | let texel_offset = 0.5 / (g.0.len() as f32); 113 | let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0)); 114 | ui.add( 115 | Image::from_texture((tex.id(), GRADIENT_SIZE)) 116 | .tint(vertex_color) 117 | .uv(uv), 118 | ) 119 | .on_hover_text(format!("A texture that is {} texels wide", g.0.len())); 120 | ui.label("GPU result"); 121 | }); 122 | }); 123 | 124 | ui.separator(); 125 | 126 | // TODO(emilk): test color multiplication (image tint), 127 | // to make sure vertex and texture color multiplication is done in linear space. 128 | 129 | ui.label("Gamma interpolation:"); 130 | self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma); 131 | 132 | ui.separator(); 133 | 134 | self.show_gradients(ui, RED, (TRANSPARENT, GREEN), Interpolation::Gamma); 135 | 136 | ui.separator(); 137 | 138 | self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN), Interpolation::Gamma); 139 | 140 | ui.separator(); 141 | 142 | self.show_gradients(ui, BLACK, (BLACK, WHITE), Interpolation::Gamma); 143 | ui.separator(); 144 | self.show_gradients(ui, WHITE, (BLACK, TRANSPARENT), Interpolation::Gamma); 145 | ui.separator(); 146 | self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE), Interpolation::Gamma); 147 | ui.separator(); 148 | 149 | ui.label("Additive blending: add more and more blue to the red background:"); 150 | self.show_gradients( 151 | ui, 152 | RED, 153 | (TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)), 154 | Interpolation::Gamma, 155 | ); 156 | 157 | ui.separator(); 158 | 159 | ui.label("Linear interpolation (texture sampling):"); 160 | self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear); 161 | } 162 | 163 | fn show_gradients( 164 | &mut self, 165 | ui: &mut Ui, 166 | bg_fill: Color32, 167 | (left, right): (Color32, Color32), 168 | interpolation: Interpolation, 169 | ) { 170 | let is_opaque = left.is_opaque() && right.is_opaque(); 171 | 172 | ui.horizontal(|ui| { 173 | let color_size = ui.spacing().interact_size; 174 | if !is_opaque { 175 | ui.label("Background:"); 176 | show_color(ui, bg_fill, color_size); 177 | } 178 | ui.label("gradient"); 179 | show_color(ui, left, color_size); 180 | ui.label("-"); 181 | show_color(ui, right, color_size); 182 | }); 183 | 184 | ui.scope(|ui| { 185 | ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients 186 | if is_opaque { 187 | let g = Gradient::ground_truth_gradient(left, right, interpolation); 188 | self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g); 189 | self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g); 190 | } else { 191 | let g = Gradient::ground_truth_gradient(left, right, interpolation) 192 | .with_bg_fill(bg_fill); 193 | self.vertex_gradient( 194 | ui, 195 | "Ground Truth (CPU gradient, CPU blending) - vertices", 196 | bg_fill, 197 | &g, 198 | ); 199 | self.tex_gradient( 200 | ui, 201 | "Ground Truth (CPU gradient, CPU blending) - texture", 202 | bg_fill, 203 | &g, 204 | ); 205 | let g = Gradient::ground_truth_gradient(left, right, interpolation); 206 | self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g); 207 | self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g); 208 | } 209 | 210 | let g = Gradient::endpoints(left, right); 211 | 212 | match interpolation { 213 | Interpolation::Linear => { 214 | // texture sampler is sRGBA aware, and should therefore be linear 215 | self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g); 216 | } 217 | Interpolation::Gamma => { 218 | // vertex shader uses gamma 219 | self.vertex_gradient( 220 | ui, 221 | "Triangle mesh of width 2 (test vertex decode and interpolation)", 222 | bg_fill, 223 | &g, 224 | ); 225 | } 226 | } 227 | }); 228 | } 229 | 230 | fn tex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) { 231 | if !self.texture_gradients { 232 | return; 233 | } 234 | ui.horizontal(|ui| { 235 | let tex = self.tex_mngr.get(ui.ctx(), gradient); 236 | let texel_offset = 0.5 / (gradient.0.len() as f32); 237 | let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0)); 238 | ui.add( 239 | Image::from_texture((tex.id(), GRADIENT_SIZE)) 240 | .bg_fill(bg_fill) 241 | .uv(uv), 242 | ) 243 | .on_hover_text(format!( 244 | "A texture that is {} texels wide", 245 | gradient.0.len() 246 | )); 247 | ui.label(label); 248 | }); 249 | } 250 | 251 | fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) { 252 | if !self.vertex_gradients { 253 | return; 254 | } 255 | ui.horizontal(|ui| { 256 | vertex_gradient(ui, bg_fill, gradient).on_hover_text(format!( 257 | "A triangle mesh that is {} vertices wide", 258 | gradient.0.len() 259 | )); 260 | ui.label(label); 261 | }); 262 | } 263 | } 264 | 265 | fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Response { 266 | use egui::epaint::*; 267 | let (rect, response) = ui.allocate_at_least(GRADIENT_SIZE, Sense::hover()); 268 | if bg_fill != Default::default() { 269 | let mut mesh = Mesh::default(); 270 | mesh.add_colored_rect(rect, bg_fill); 271 | ui.painter().add(Shape::mesh(mesh)); 272 | } 273 | { 274 | let n = gradient.0.len(); 275 | assert!(n >= 2); 276 | let mut mesh = Mesh::default(); 277 | for (i, &color) in gradient.0.iter().enumerate() { 278 | let t = i as f32 / (n as f32 - 1.0); 279 | let x = lerp(rect.x_range(), t); 280 | mesh.colored_vertex(pos2(x, rect.top()), color); 281 | mesh.colored_vertex(pos2(x, rect.bottom()), color); 282 | if i < n - 1 { 283 | let i = i as u32; 284 | mesh.add_triangle(2 * i, 2 * i + 1, 2 * i + 2); 285 | mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3); 286 | } 287 | } 288 | ui.painter().add(Shape::mesh(mesh)); 289 | } 290 | response 291 | } 292 | 293 | #[derive(Clone, Copy)] 294 | enum Interpolation { 295 | Linear, 296 | Gamma, 297 | } 298 | 299 | #[derive(Clone, Hash, PartialEq, Eq)] 300 | struct Gradient(pub Vec); 301 | 302 | impl Gradient { 303 | pub fn one_color(srgba: Color32) -> Self { 304 | Self(vec![srgba, srgba]) 305 | } 306 | 307 | pub fn endpoints(left: Color32, right: Color32) -> Self { 308 | Self(vec![left, right]) 309 | } 310 | 311 | pub fn ground_truth_gradient( 312 | left: Color32, 313 | right: Color32, 314 | interpolation: Interpolation, 315 | ) -> Self { 316 | match interpolation { 317 | Interpolation::Linear => Self::ground_truth_linear_gradient(left, right), 318 | Interpolation::Gamma => Self::ground_truth_gamma_gradient(left, right), 319 | } 320 | } 321 | 322 | pub fn ground_truth_linear_gradient(left: Color32, right: Color32) -> Self { 323 | let left = Rgba::from(left); 324 | let right = Rgba::from(right); 325 | 326 | let n = 255; 327 | Self( 328 | (0..=n) 329 | .map(|i| { 330 | let t = i as f32 / n as f32; 331 | Color32::from(lerp(left..=right, t)) 332 | }) 333 | .collect(), 334 | ) 335 | } 336 | 337 | pub fn ground_truth_gamma_gradient(left: Color32, right: Color32) -> Self { 338 | let n = 255; 339 | Self( 340 | (0..=n) 341 | .map(|i| { 342 | let t = i as f32 / n as f32; 343 | lerp_color_gamma(left, right, t) 344 | }) 345 | .collect(), 346 | ) 347 | } 348 | 349 | /// Do premultiplied alpha-aware blending of the gradient on top of the fill color 350 | /// in gamma-space. 351 | pub fn with_bg_fill(self, bg: Color32) -> Self { 352 | Self( 353 | self.0 354 | .into_iter() 355 | .map(|fg| { 356 | let a = fg.a() as f32 / 255.0; 357 | Color32::from_rgba_premultiplied( 358 | (bg[0] as f32 * (1.0 - a) + fg[0] as f32).round() as u8, 359 | (bg[1] as f32 * (1.0 - a) + fg[1] as f32).round() as u8, 360 | (bg[2] as f32 * (1.0 - a) + fg[2] as f32).round() as u8, 361 | (bg[3] as f32 * (1.0 - a) + fg[3] as f32).round() as u8, 362 | ) 363 | }) 364 | .collect(), 365 | ) 366 | } 367 | 368 | pub fn to_pixel_row(&self) -> Vec { 369 | self.0.clone() 370 | } 371 | } 372 | 373 | #[derive(Default)] 374 | struct TextureManager(HashMap); 375 | 376 | impl TextureManager { 377 | fn get(&mut self, ctx: &egui::Context, gradient: &Gradient) -> &TextureHandle { 378 | self.0.entry(gradient.clone()).or_insert_with(|| { 379 | let pixels = gradient.to_pixel_row(); 380 | let width = pixels.len(); 381 | let height = 1; 382 | ctx.load_texture( 383 | "color_test_gradient", 384 | epaint::ColorImage { 385 | size: [width, height], 386 | pixels, 387 | }, 388 | TextureOptions::LINEAR, 389 | ) 390 | }) 391 | } 392 | } 393 | 394 | /// A visual test that the rendering is correctly aligned on the physical pixel grid. 395 | /// 396 | /// Requires eyes and a magnifying glass to verify. 397 | pub fn pixel_test(ui: &mut Ui) { 398 | ui.heading("Pixel alignment test"); 399 | ui.label("If anything is blurry, then everything will be blurry, including text."); 400 | ui.label("You might need a magnifying glass to check this test."); 401 | 402 | if cfg!(target_arch = "wasm32") { 403 | ui.label("Make sure these test pass even when you zoom in/out and resize the browser."); 404 | } 405 | 406 | ui.add_space(4.0); 407 | 408 | pixel_test_lines(ui); 409 | 410 | ui.add_space(4.0); 411 | 412 | pixel_test_squares(ui); 413 | } 414 | 415 | fn pixel_test_squares(ui: &mut Ui) { 416 | ui.label("The first square should be exactly one physical pixel big."); 417 | ui.label("They should be exactly one physical pixel apart."); 418 | ui.label("Each subsequent square should be one physical pixel larger than the previous."); 419 | ui.label("They should be perfectly aligned to the physical pixel grid."); 420 | 421 | let color = if ui.style().visuals.dark_mode { 422 | egui::Color32::WHITE 423 | } else { 424 | egui::Color32::BLACK 425 | }; 426 | 427 | let pixels_per_point = ui.ctx().pixels_per_point(); 428 | 429 | let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32; 430 | let size_pixels = vec2( 431 | ((num_squares + 1) * (num_squares + 2) / 2) as f32, 432 | num_squares as f32, 433 | ); 434 | let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0); 435 | let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); 436 | 437 | let mut cursor_pixel = Pos2::new( 438 | response.rect.min.x * pixels_per_point, 439 | response.rect.min.y * pixels_per_point, 440 | ) 441 | .ceil(); 442 | for size in 1..=num_squares { 443 | let rect_points = Rect::from_min_size( 444 | Pos2::new(cursor_pixel.x, cursor_pixel.y), 445 | Vec2::splat(size as f32), 446 | ); 447 | painter.rect_filled(rect_points / pixels_per_point, 0.0, color); 448 | cursor_pixel.x += (1 + size) as f32; 449 | } 450 | } 451 | 452 | fn pixel_test_lines(ui: &mut Ui) { 453 | let pixels_per_point = ui.ctx().pixels_per_point(); 454 | let n = (96.0 * pixels_per_point) as usize; 455 | 456 | ui.label("The lines should be exactly one physical pixel wide, one physical pixel apart."); 457 | ui.label("They should be perfectly white and black."); 458 | 459 | let hspace_px = pixels_per_point * 4.0; 460 | 461 | let size_px = Vec2::new(2.0 * n as f32 + hspace_px, n as f32); 462 | let size_points = size_px / pixels_per_point + Vec2::splat(2.0); 463 | let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); 464 | 465 | let mut cursor_px = Pos2::new( 466 | response.rect.min.x * pixels_per_point, 467 | response.rect.min.y * pixels_per_point, 468 | ) 469 | .ceil(); 470 | 471 | // Vertical stripes: 472 | for x in 0..n / 2 { 473 | let rect_px = Rect::from_min_size( 474 | Pos2::new(cursor_px.x + 2.0 * x as f32, cursor_px.y), 475 | Vec2::new(1.0, n as f32), 476 | ); 477 | painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::WHITE); 478 | let rect_px = rect_px.translate(vec2(1.0, 0.0)); 479 | painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::BLACK); 480 | } 481 | 482 | cursor_px.x += n as f32 + hspace_px; 483 | 484 | // Horizontal stripes: 485 | for y in 0..n / 2 { 486 | let rect_px = Rect::from_min_size( 487 | Pos2::new(cursor_px.x, cursor_px.y + 2.0 * y as f32), 488 | Vec2::new(n as f32, 1.0), 489 | ); 490 | painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::WHITE); 491 | let rect_px = rect_px.translate(vec2(0.0, 1.0)); 492 | painter.rect_filled(rect_px / pixels_per_point, 0.0, egui::Color32::BLACK); 493 | } 494 | } 495 | 496 | fn blending_and_feathering_test(ui: &mut Ui) { 497 | ui.label("The left side shows how lines of different widths look."); 498 | ui.label("The right side tests text rendering at different opacities and sizes."); 499 | ui.label("The top and bottom images should look symmetrical in their intensities."); 500 | 501 | let size = vec2(512.0, 512.0); 502 | let (response, painter) = ui.allocate_painter(size, Sense::hover()); 503 | let rect = response.rect; 504 | 505 | let mut top_half = rect; 506 | top_half.set_bottom(top_half.center().y); 507 | painter.rect_filled(top_half, 0.0, Color32::BLACK); 508 | paint_fine_lines_and_text(&painter, top_half, Color32::WHITE); 509 | 510 | let mut bottom_half = rect; 511 | bottom_half.set_top(bottom_half.center().y); 512 | painter.rect_filled(bottom_half, 0.0, Color32::WHITE); 513 | paint_fine_lines_and_text(&painter, bottom_half, Color32::BLACK); 514 | } 515 | 516 | fn text_on_bg(ui: &mut egui::Ui, fg: Color32, bg: Color32) { 517 | assert!(fg.is_opaque()); 518 | assert!(bg.is_opaque()); 519 | 520 | ui.horizontal(|ui| { 521 | ui.label( 522 | RichText::from("▣ The quick brown fox jumps over the lazy dog and runs away.") 523 | .background_color(bg) 524 | .color(fg), 525 | ); 526 | ui.label(format!( 527 | "({} {} {}) on ({} {} {})", 528 | fg.r(), 529 | fg.g(), 530 | fg.b(), 531 | bg.r(), 532 | bg.g(), 533 | bg.b(), 534 | )); 535 | }); 536 | } 537 | 538 | fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Color32) { 539 | { 540 | let mut y = 0.0; 541 | for opacity in [1.00, 0.50, 0.25, 0.10, 0.05, 0.02, 0.01, 0.00] { 542 | painter.text( 543 | rect.center_top() + vec2(0.0, y), 544 | Align2::LEFT_TOP, 545 | format!("{:.0}% white", 100.0 * opacity), 546 | FontId::proportional(14.0), 547 | Color32::WHITE.gamma_multiply(opacity), 548 | ); 549 | painter.text( 550 | rect.center_top() + vec2(80.0, y), 551 | Align2::LEFT_TOP, 552 | format!("{:.0}% gray", 100.0 * opacity), 553 | FontId::proportional(14.0), 554 | Color32::GRAY.gamma_multiply(opacity), 555 | ); 556 | painter.text( 557 | rect.center_top() + vec2(160.0, y), 558 | Align2::LEFT_TOP, 559 | format!("{:.0}% black", 100.0 * opacity), 560 | FontId::proportional(14.0), 561 | Color32::BLACK.gamma_multiply(opacity), 562 | ); 563 | y += 20.0; 564 | } 565 | 566 | for font_size in [6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 14.0] { 567 | painter.text( 568 | rect.center_top() + vec2(0.0, y), 569 | Align2::LEFT_TOP, 570 | format!( 571 | "{font_size}px - The quick brown fox jumps over the lazy dog and runs away." 572 | ), 573 | FontId::proportional(font_size), 574 | color, 575 | ); 576 | y += font_size + 1.0; 577 | } 578 | } 579 | 580 | rect.max.x = rect.center().x; 581 | 582 | rect = rect.shrink(16.0); 583 | for width in [0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0] { 584 | painter.text( 585 | rect.left_top(), 586 | Align2::CENTER_CENTER, 587 | width.to_string(), 588 | FontId::monospace(12.0), 589 | color, 590 | ); 591 | 592 | painter.add(egui::epaint::CubicBezierShape::from_points_stroke( 593 | [ 594 | rect.left_top() + vec2(16.0, 0.0), 595 | rect.right_top(), 596 | rect.right_center(), 597 | rect.right_bottom(), 598 | ], 599 | false, 600 | Color32::TRANSPARENT, 601 | Stroke::new(width, color), 602 | )); 603 | 604 | rect.min.y += 24.0; 605 | rect.max.x -= 24.0; 606 | } 607 | 608 | rect.min.y += 16.0; 609 | painter.text( 610 | rect.left_top(), 611 | Align2::LEFT_CENTER, 612 | "transparent --> opaque", 613 | FontId::monospace(10.0), 614 | color, 615 | ); 616 | rect.min.y += 12.0; 617 | let mut mesh = Mesh::default(); 618 | mesh.colored_vertex(rect.left_bottom(), Color32::TRANSPARENT); 619 | mesh.colored_vertex(rect.left_top(), Color32::TRANSPARENT); 620 | mesh.colored_vertex(rect.right_bottom(), color); 621 | mesh.colored_vertex(rect.right_top(), color); 622 | mesh.add_triangle(0, 1, 2); 623 | mesh.add_triangle(1, 2, 3); 624 | painter.add(mesh); 625 | } 626 | 627 | fn mul_color_gamma(left: Color32, right: Color32) -> Color32 { 628 | Color32::from_rgba_premultiplied( 629 | (left.r() as f32 * right.r() as f32 / 255.0).round() as u8, 630 | (left.g() as f32 * right.g() as f32 / 255.0).round() as u8, 631 | (left.b() as f32 * right.b() as f32 / 255.0).round() as u8, 632 | (left.a() as f32 * right.a() as f32 / 255.0).round() as u8, 633 | ) 634 | } 635 | 636 | fn lerp_color_gamma(left: Color32, right: Color32, t: f32) -> Color32 { 637 | Color32::from_rgba_premultiplied( 638 | lerp((left[0] as f32)..=(right[0] as f32), t).round() as u8, 639 | lerp((left[1] as f32)..=(right[1] as f32), t).round() as u8, 640 | lerp((left[2] as f32)..=(right[2] as f32), t).round() as u8, 641 | lerp((left[3] as f32)..=(right[3] as f32), t).round() as u8, 642 | ) 643 | } 644 | 645 | fn main() { 646 | let settings = WindowOpenOptions { 647 | title: String::from("egui-baseview hello world"), 648 | size: Size::new(1280.0, 720.0), 649 | scale: WindowScalePolicy::SystemScaleFactor, 650 | #[cfg(feature = "opengl")] 651 | gl_config: Some(Default::default()), 652 | }; 653 | 654 | let state = ColorTest::default(); 655 | 656 | EguiWindow::open_blocking( 657 | settings, 658 | GraphicsConfig::default(), 659 | state, 660 | |_egui_ctx: &Context, _queue: &mut Queue, _state: &mut ColorTest| {}, 661 | |egui_ctx: &Context, _queue: &mut Queue, state: &mut ColorTest| { 662 | egui::Window::new("egui-baseview hello world").show(egui_ctx, |ui| { 663 | ui.label("uwu"); 664 | }); 665 | 666 | egui::Window::new("rendering test") 667 | .scroll(true) 668 | .show(egui_ctx, |ui| { 669 | state.ui(ui); 670 | }); 671 | }, 672 | ); 673 | } 674 | -------------------------------------------------------------------------------- /examples/simple_demo.rs: -------------------------------------------------------------------------------- 1 | use baseview::{Size, WindowOpenOptions, WindowScalePolicy}; 2 | use egui::Context; 3 | use egui_baseview::{EguiWindow, GraphicsConfig, Queue}; 4 | 5 | fn main() { 6 | let settings = WindowOpenOptions { 7 | title: String::from("egui-baseview simple demo"), 8 | size: Size::new(400.0, 200.0), 9 | scale: WindowScalePolicy::SystemScaleFactor, 10 | #[cfg(feature = "opengl")] 11 | gl_config: Some(Default::default()), 12 | }; 13 | 14 | let state = State::new(); 15 | 16 | EguiWindow::open_blocking( 17 | settings, 18 | GraphicsConfig::default(), 19 | state, 20 | // Called once before the first frame. Allows you to do setup code and to 21 | // call `ctx.set_fonts()`. Optional. 22 | |_egui_ctx: &Context, _queue: &mut Queue, _state: &mut State| {}, 23 | // Called before each frame. Here you should update the state of your 24 | // application and build the UI. 25 | |egui_ctx: &Context, queue: &mut Queue, state: &mut State| { 26 | egui::Window::new("egui-baseview simple demo").show(egui_ctx, |ui| { 27 | ui.heading("My Egui Application"); 28 | ui.horizontal(|ui| { 29 | ui.label("Your name: "); 30 | ui.text_edit_singleline(&mut state.name); 31 | }); 32 | ui.add(egui::Slider::new(&mut state.age, 0..=120).text("age")); 33 | if ui.button("Click each year").clicked() { 34 | state.age += 1; 35 | } 36 | ui.label(format!("Hello '{}', age {}", state.name, state.age)); 37 | if ui.button("close window").clicked() { 38 | queue.close_window(); 39 | } 40 | 41 | ui.hyperlink_to("free crouton", "https://crouton.net"); 42 | }); 43 | }, 44 | ); 45 | } 46 | 47 | struct State { 48 | pub name: String, 49 | pub age: u32, 50 | } 51 | 52 | impl State { 53 | pub fn new() -> State { 54 | State { 55 | name: String::from(""), 56 | age: 30, 57 | } 58 | } 59 | } 60 | 61 | impl Drop for State { 62 | fn drop(&mut self) { 63 | println!("Window is closing!"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BillyDM/egui-baseview/ec70c3fe6b2f070dcacbc22924431edbe24bd1c0/screenshot.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod renderer; 2 | mod translate; 3 | mod window; 4 | 5 | pub use window::{EguiWindow, Queue}; 6 | 7 | pub use egui; 8 | pub use renderer::GraphicsConfig; 9 | -------------------------------------------------------------------------------- /src/renderer.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "opengl")] 2 | mod opengl; 3 | #[cfg(feature = "opengl")] 4 | pub use opengl::renderer::{GraphicsConfig, Renderer}; 5 | 6 | #[cfg(feature = "wgpu")] 7 | mod wgpu; 8 | #[cfg(feature = "wgpu")] 9 | pub use wgpu::renderer::{GraphicsConfig, Renderer}; 10 | -------------------------------------------------------------------------------- /src/renderer/opengl.rs: -------------------------------------------------------------------------------- 1 | use egui_glow::PainterError; 2 | use thiserror::Error; 3 | 4 | pub mod renderer; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum OpenGlError { 8 | #[error("Failed to get baseview's GL context")] 9 | NoContext, 10 | #[error("Error occured when initializing painter: \n {0}")] 11 | CreatePainter(PainterError), 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/opengl/renderer.rs: -------------------------------------------------------------------------------- 1 | use baseview::{PhySize, Window}; 2 | use egui::FullOutput; 3 | use egui_glow::Painter; 4 | use std::sync::Arc; 5 | 6 | use super::OpenGlError; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct GraphicsConfig { 10 | /// Controls whether to apply dithering to minimize banding artifacts. 11 | /// 12 | /// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between 13 | /// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space". 14 | /// This means that only inputs from texture interpolation and vertex colors should be affected in practice. 15 | /// 16 | /// Defaults to true. 17 | pub dithering: bool, 18 | 19 | /// Needed for cross compiling for VirtualBox VMSVGA driver with OpenGL ES 2.0 and OpenGL 2.1 which doesn't support SRGB texture. 20 | /// See . 21 | /// 22 | /// For OpenGL ES 2.0: set this to [`egui_glow::ShaderVersion::Es100`] to solve blank texture problem (by using the "fallback shader"). 23 | pub shader_version: Option, 24 | } 25 | 26 | impl Default for GraphicsConfig { 27 | fn default() -> Self { 28 | Self { 29 | shader_version: None, 30 | dithering: true, 31 | } 32 | } 33 | } 34 | 35 | pub struct Renderer { 36 | glow_context: Arc, 37 | painter: Painter, 38 | } 39 | 40 | impl Renderer { 41 | pub fn new(window: &Window, config: GraphicsConfig) -> Result { 42 | let context = window.gl_context().ok_or(OpenGlError::NoContext)?; 43 | unsafe { 44 | context.make_current(); 45 | } 46 | 47 | #[allow(clippy::arc_with_non_send_sync)] 48 | let glow_context = Arc::new(unsafe { 49 | egui_glow::glow::Context::from_loader_function(|s| context.get_proc_address(s)) 50 | }); 51 | 52 | let painter = egui_glow::Painter::new( 53 | Arc::clone(&glow_context), 54 | "", 55 | config.shader_version, 56 | config.dithering, 57 | ) 58 | .map_err(OpenGlError::CreatePainter)?; 59 | 60 | unsafe { 61 | context.make_not_current(); 62 | } 63 | 64 | Ok(Self { 65 | glow_context, 66 | painter, 67 | }) 68 | } 69 | 70 | pub fn max_texture_side(&self) -> usize { 71 | self.painter.max_texture_side() 72 | } 73 | 74 | pub fn render( 75 | &mut self, 76 | window: &Window, 77 | bg_color: egui::Rgba, 78 | physical_size: PhySize, 79 | pixels_per_point: f32, 80 | egui_ctx: &mut egui::Context, 81 | full_output: &mut FullOutput, 82 | ) { 83 | let PhySize { 84 | width: canvas_width, 85 | height: canvas_height, 86 | } = physical_size; 87 | 88 | let shapes = std::mem::take(&mut full_output.shapes); 89 | let textures_delta = &mut full_output.textures_delta; 90 | 91 | let context = window 92 | .gl_context() 93 | .expect("failed to get baseview gl context"); 94 | unsafe { 95 | context.make_current(); 96 | } 97 | 98 | unsafe { 99 | use egui_glow::glow::HasContext as _; 100 | self.glow_context 101 | .clear_color(bg_color.r(), bg_color.g(), bg_color.b(), bg_color.a()); 102 | self.glow_context.clear(egui_glow::glow::COLOR_BUFFER_BIT); 103 | } 104 | 105 | for (id, image_delta) in &textures_delta.set { 106 | self.painter.set_texture(*id, image_delta); 107 | } 108 | 109 | let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); 110 | let dimensions: [u32; 2] = [canvas_width, canvas_height]; 111 | 112 | self.painter 113 | .paint_primitives(dimensions, pixels_per_point, &clipped_primitives); 114 | 115 | for id in textures_delta.free.drain(..) { 116 | self.painter.free_texture(id); 117 | } 118 | 119 | unsafe { 120 | context.swap_buffers(); 121 | context.make_not_current(); 122 | } 123 | } 124 | } 125 | 126 | impl Drop for Renderer { 127 | fn drop(&mut self) { 128 | self.painter.destroy() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/renderer/wgpu.rs: -------------------------------------------------------------------------------- 1 | pub mod renderer; 2 | -------------------------------------------------------------------------------- /src/renderer/wgpu/renderer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | num::{NonZeroIsize, NonZeroU32}, 3 | ptr::NonNull, 4 | sync::Arc, 5 | }; 6 | 7 | use baseview::{PhySize, Window}; 8 | use egui::FullOutput; 9 | use egui_wgpu::{ 10 | wgpu::{ 11 | Color, CommandEncoderDescriptor, Extent3d, Instance, InstanceDescriptor, 12 | RenderPassColorAttachment, RenderPassDescriptor, Surface, SurfaceConfiguration, 13 | SurfaceTargetUnsafe, TextureDescriptor, TextureDimension, TextureUsages, TextureView, 14 | TextureViewDescriptor, 15 | }, 16 | RenderState, ScreenDescriptor, WgpuError, 17 | }; 18 | use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; 19 | use raw_window_handle_06::{ 20 | AppKitDisplayHandle, AppKitWindowHandle, Win32WindowHandle, WindowsDisplayHandle, 21 | XcbDisplayHandle, XcbWindowHandle, XlibDisplayHandle, XlibWindowHandle, 22 | }; 23 | 24 | pub use egui_wgpu::WgpuConfiguration; 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct GraphicsConfig { 28 | /// Controls whether to apply dithering to minimize banding artifacts. 29 | /// 30 | /// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between 31 | /// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space". 32 | /// This means that only inputs from texture interpolation and vertex colors should be affected in practice. 33 | /// 34 | /// Defaults to true. 35 | pub dithering: bool, 36 | 37 | /// Configures wgpu instance/device/adapter/surface creation and renderloop. 38 | pub wgpu_options: WgpuConfiguration, 39 | } 40 | 41 | impl Default for GraphicsConfig { 42 | fn default() -> Self { 43 | Self { 44 | dithering: true, 45 | wgpu_options: Default::default(), 46 | } 47 | } 48 | } 49 | 50 | const MSAA_SAMPLES: u32 = 4; 51 | 52 | pub struct Renderer { 53 | render_state: Arc, 54 | surface: Surface<'static>, 55 | config: GraphicsConfig, 56 | msaa_texture_view: Option, 57 | width: u32, 58 | height: u32, 59 | } 60 | 61 | impl Renderer { 62 | pub fn new(window: &Window, config: GraphicsConfig) -> Result { 63 | let instance = Instance::new(InstanceDescriptor::default()); 64 | 65 | let raw_display_handle = window.raw_display_handle(); 66 | let raw_window_handle = window.raw_window_handle(); 67 | 68 | let target = SurfaceTargetUnsafe::RawHandle { 69 | raw_display_handle: match raw_display_handle { 70 | raw_window_handle::RawDisplayHandle::AppKit(_) => { 71 | raw_window_handle_06::RawDisplayHandle::AppKit(AppKitDisplayHandle::new()) 72 | } 73 | raw_window_handle::RawDisplayHandle::Xlib(handle) => { 74 | raw_window_handle_06::RawDisplayHandle::Xlib(XlibDisplayHandle::new( 75 | NonNull::new(handle.display), 76 | handle.screen, 77 | )) 78 | } 79 | raw_window_handle::RawDisplayHandle::Xcb(handle) => { 80 | raw_window_handle_06::RawDisplayHandle::Xcb(XcbDisplayHandle::new( 81 | NonNull::new(handle.connection), 82 | handle.screen, 83 | )) 84 | } 85 | raw_window_handle::RawDisplayHandle::Windows(_) => { 86 | raw_window_handle_06::RawDisplayHandle::Windows(WindowsDisplayHandle::new()) 87 | } 88 | _ => todo!(), 89 | }, 90 | raw_window_handle: match raw_window_handle { 91 | raw_window_handle::RawWindowHandle::AppKit(handle) => { 92 | raw_window_handle_06::RawWindowHandle::AppKit(AppKitWindowHandle::new( 93 | NonNull::new(handle.ns_view).unwrap(), 94 | )) 95 | } 96 | raw_window_handle::RawWindowHandle::Xlib(handle) => { 97 | raw_window_handle_06::RawWindowHandle::Xlib(XlibWindowHandle::new( 98 | handle.window, 99 | )) 100 | } 101 | raw_window_handle::RawWindowHandle::Xcb(handle) => { 102 | raw_window_handle_06::RawWindowHandle::Xcb(XcbWindowHandle::new( 103 | NonZeroU32::new(handle.window).unwrap(), 104 | )) 105 | } 106 | raw_window_handle::RawWindowHandle::Win32(handle) => { 107 | // will this work? i have no idea! 108 | let mut raw_handle = 109 | Win32WindowHandle::new(NonZeroIsize::new(handle.hwnd as isize).unwrap()); 110 | 111 | raw_handle.hinstance = handle 112 | .hinstance 113 | .is_null() 114 | .then(|| NonZeroIsize::new(handle.hinstance as isize).unwrap()); 115 | 116 | raw_window_handle_06::RawWindowHandle::Win32(raw_handle) 117 | } 118 | _ => todo!(), 119 | }, 120 | }; 121 | 122 | let surface = unsafe { instance.create_surface_unsafe(target) }.unwrap(); 123 | 124 | let state = Arc::new(pollster::block_on(RenderState::create( 125 | &config.wgpu_options, 126 | &instance, 127 | &surface, 128 | None, 129 | MSAA_SAMPLES, 130 | config.dithering, 131 | ))?); 132 | 133 | Ok(Self { 134 | render_state: state, 135 | surface, 136 | config, 137 | msaa_texture_view: None, 138 | width: 0, 139 | height: 0, 140 | }) 141 | } 142 | 143 | pub fn max_texture_side(&self) -> usize { 144 | self.render_state 145 | .as_ref() 146 | .device 147 | .limits() 148 | .max_texture_dimension_2d as usize 149 | } 150 | 151 | fn configure_surface(&self, width: u32, height: u32) { 152 | let usage = TextureUsages::RENDER_ATTACHMENT; 153 | 154 | let mut surf_config = SurfaceConfiguration { 155 | usage, 156 | format: self.render_state.target_format, 157 | present_mode: self.config.wgpu_options.present_mode, 158 | view_formats: vec![self.render_state.target_format], 159 | ..self 160 | .surface 161 | .get_default_config(&self.render_state.adapter, width, height) 162 | .expect("Unsupported surface") 163 | }; 164 | 165 | if let Some(desired_maximum_frame_latency) = 166 | self.config.wgpu_options.desired_maximum_frame_latency 167 | { 168 | surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency; 169 | } 170 | 171 | self.surface 172 | .configure(&self.render_state.device, &surf_config); 173 | } 174 | 175 | fn resize_and_generate_msaa_view(&mut self, width: u32, height: u32) { 176 | let render_state = self.render_state.as_ref(); 177 | 178 | self.width = width; 179 | self.height = height; 180 | 181 | self.configure_surface(width, height); 182 | 183 | let texture_format = render_state.target_format; 184 | self.msaa_texture_view = Some( 185 | render_state 186 | .device 187 | .create_texture(&TextureDescriptor { 188 | label: Some("egui_msaa_texture"), 189 | size: Extent3d { 190 | width, 191 | height, 192 | depth_or_array_layers: 1, 193 | }, 194 | mip_level_count: 1, 195 | sample_count: MSAA_SAMPLES, 196 | dimension: TextureDimension::D2, 197 | format: texture_format, 198 | usage: TextureUsages::RENDER_ATTACHMENT, 199 | view_formats: &[texture_format], 200 | }) 201 | .create_view(&TextureViewDescriptor::default()), 202 | ); 203 | } 204 | 205 | pub fn render( 206 | &mut self, 207 | bg_color: egui::Rgba, 208 | physical_size: PhySize, 209 | pixels_per_point: f32, 210 | egui_ctx: &mut egui::Context, 211 | full_output: &mut FullOutput, 212 | ) { 213 | let PhySize { 214 | width: canvas_width, 215 | height: canvas_height, 216 | } = physical_size; 217 | 218 | let shapes = std::mem::take(&mut full_output.shapes); 219 | 220 | let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); 221 | 222 | let mut encoder = 223 | self.render_state 224 | .device 225 | .create_command_encoder(&CommandEncoderDescriptor { 226 | label: Some("encoder"), 227 | }); 228 | 229 | let screen_descriptor = ScreenDescriptor { 230 | size_in_pixels: [canvas_width, canvas_height], 231 | pixels_per_point, 232 | }; 233 | 234 | let user_cmd_bufs = { 235 | let mut renderer = self.render_state.renderer.write(); 236 | for (id, image_delta) in &full_output.textures_delta.set { 237 | renderer.update_texture( 238 | &self.render_state.device, 239 | &self.render_state.queue, 240 | *id, 241 | image_delta, 242 | ); 243 | } 244 | 245 | renderer.update_buffers( 246 | &self.render_state.device, 247 | &self.render_state.queue, 248 | &mut encoder, 249 | &clipped_primitives, 250 | &screen_descriptor, 251 | ) 252 | }; 253 | 254 | if self.width != canvas_width 255 | || self.height != canvas_height 256 | || self.msaa_texture_view.is_none() 257 | { 258 | self.resize_and_generate_msaa_view(canvas_width, canvas_height); 259 | } 260 | 261 | let output_frame = { self.surface.get_current_texture() }; 262 | 263 | let output_frame = match output_frame { 264 | Ok(frame) => frame, 265 | Err(err) => match (self.config.wgpu_options.on_surface_error)(err) { 266 | egui_wgpu::SurfaceErrorAction::SkipFrame => return, 267 | egui_wgpu::SurfaceErrorAction::RecreateSurface => { 268 | self.configure_surface(self.width, self.height); 269 | return; 270 | } 271 | }, 272 | }; 273 | 274 | { 275 | let renderer = self.render_state.renderer.read(); 276 | let frame_view = output_frame 277 | .texture 278 | .create_view(&TextureViewDescriptor::default()); 279 | 280 | let render_pass = encoder.begin_render_pass(&RenderPassDescriptor { 281 | label: Some("egui_render"), 282 | color_attachments: &[Some(RenderPassColorAttachment { 283 | view: self.msaa_texture_view.as_ref().unwrap(), 284 | resolve_target: Some(&frame_view), 285 | ops: egui_wgpu::wgpu::Operations { 286 | load: egui_wgpu::wgpu::LoadOp::Clear(Color { 287 | r: bg_color[0] as f64, 288 | g: bg_color[1] as f64, 289 | b: bg_color[2] as f64, 290 | a: bg_color[3] as f64, 291 | }), 292 | store: egui_wgpu::wgpu::StoreOp::Store, 293 | }, 294 | })], 295 | depth_stencil_attachment: None, 296 | timestamp_writes: None, 297 | occlusion_query_set: None, 298 | }); 299 | 300 | // Forgetting the pass' lifetime means that we are no longer compile-time protected from 301 | // runtime errors caused by accessing the parent encoder before the render pass is dropped. 302 | // Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here! 303 | renderer.render( 304 | &mut render_pass.forget_lifetime(), 305 | &clipped_primitives, 306 | &screen_descriptor, 307 | ); 308 | } 309 | 310 | { 311 | let mut renderer = self.render_state.renderer.write(); 312 | for id in &full_output.textures_delta.free { 313 | renderer.free_texture(id); 314 | } 315 | } 316 | 317 | let encoded = encoder.finish(); 318 | 319 | self.render_state 320 | .queue 321 | .submit(user_cmd_bufs.into_iter().chain([encoded])); 322 | 323 | output_frame.present(); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/translate.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn translate_mouse_button(button: baseview::MouseButton) -> Option { 2 | match button { 3 | baseview::MouseButton::Left => Some(egui::PointerButton::Primary), 4 | baseview::MouseButton::Right => Some(egui::PointerButton::Secondary), 5 | baseview::MouseButton::Middle => Some(egui::PointerButton::Middle), 6 | _ => None, 7 | } 8 | } 9 | 10 | pub(crate) fn translate_virtual_key(key: &keyboard_types::Key) -> Option { 11 | use egui::Key; 12 | use keyboard_types::Key as K; 13 | 14 | Some(match key { 15 | K::ArrowDown => Key::ArrowDown, 16 | K::ArrowLeft => Key::ArrowLeft, 17 | K::ArrowRight => Key::ArrowRight, 18 | K::ArrowUp => Key::ArrowUp, 19 | 20 | K::Escape => Key::Escape, 21 | K::Tab => Key::Tab, 22 | K::Backspace => Key::Backspace, 23 | K::Enter => Key::Enter, 24 | 25 | K::Insert => Key::Insert, 26 | K::Delete => Key::Delete, 27 | K::Home => Key::Home, 28 | K::End => Key::End, 29 | K::PageUp => Key::PageUp, 30 | K::PageDown => Key::PageDown, 31 | 32 | K::Character(s) => match s.chars().next()? { 33 | ' ' => Key::Space, 34 | '0' => Key::Num0, 35 | '1' => Key::Num1, 36 | '2' => Key::Num2, 37 | '3' => Key::Num3, 38 | '4' => Key::Num4, 39 | '5' => Key::Num5, 40 | '6' => Key::Num6, 41 | '7' => Key::Num7, 42 | '8' => Key::Num8, 43 | '9' => Key::Num9, 44 | 'a' => Key::A, 45 | 'b' => Key::B, 46 | 'c' => Key::C, 47 | 'd' => Key::D, 48 | 'e' => Key::E, 49 | 'f' => Key::F, 50 | 'g' => Key::G, 51 | 'h' => Key::H, 52 | 'i' => Key::I, 53 | 'j' => Key::J, 54 | 'k' => Key::K, 55 | 'l' => Key::L, 56 | 'm' => Key::M, 57 | 'n' => Key::N, 58 | 'o' => Key::O, 59 | 'p' => Key::P, 60 | 'q' => Key::Q, 61 | 'r' => Key::R, 62 | 's' => Key::S, 63 | 't' => Key::T, 64 | 'u' => Key::U, 65 | 'v' => Key::V, 66 | 'w' => Key::W, 67 | 'x' => Key::X, 68 | 'y' => Key::Y, 69 | 'z' => Key::Z, 70 | _ => { 71 | return None; 72 | } 73 | }, 74 | _ => { 75 | return None; 76 | } 77 | }) 78 | } 79 | 80 | pub(crate) fn translate_cursor_icon(cursor: egui::CursorIcon) -> baseview::MouseCursor { 81 | match cursor { 82 | egui::CursorIcon::Default => baseview::MouseCursor::Default, 83 | egui::CursorIcon::None => baseview::MouseCursor::Hidden, 84 | egui::CursorIcon::ContextMenu => baseview::MouseCursor::Hand, 85 | egui::CursorIcon::Help => baseview::MouseCursor::Help, 86 | egui::CursorIcon::PointingHand => baseview::MouseCursor::Hand, 87 | egui::CursorIcon::Progress => baseview::MouseCursor::PtrWorking, 88 | egui::CursorIcon::Wait => baseview::MouseCursor::Working, 89 | egui::CursorIcon::Cell => baseview::MouseCursor::Cell, 90 | egui::CursorIcon::Crosshair => baseview::MouseCursor::Crosshair, 91 | egui::CursorIcon::Text => baseview::MouseCursor::Text, 92 | egui::CursorIcon::VerticalText => baseview::MouseCursor::VerticalText, 93 | egui::CursorIcon::Alias => baseview::MouseCursor::Alias, 94 | egui::CursorIcon::Copy => baseview::MouseCursor::Copy, 95 | egui::CursorIcon::Move => baseview::MouseCursor::Move, 96 | egui::CursorIcon::NoDrop => baseview::MouseCursor::NotAllowed, 97 | egui::CursorIcon::NotAllowed => baseview::MouseCursor::NotAllowed, 98 | egui::CursorIcon::Grab => baseview::MouseCursor::Hand, 99 | egui::CursorIcon::Grabbing => baseview::MouseCursor::HandGrabbing, 100 | egui::CursorIcon::AllScroll => baseview::MouseCursor::AllScroll, 101 | egui::CursorIcon::ResizeHorizontal => baseview::MouseCursor::EwResize, 102 | egui::CursorIcon::ResizeNeSw => baseview::MouseCursor::NeswResize, 103 | egui::CursorIcon::ResizeNwSe => baseview::MouseCursor::NwseResize, 104 | egui::CursorIcon::ResizeVertical => baseview::MouseCursor::NsResize, 105 | egui::CursorIcon::ResizeEast => baseview::MouseCursor::EResize, 106 | egui::CursorIcon::ResizeSouthEast => baseview::MouseCursor::SeResize, 107 | egui::CursorIcon::ResizeSouth => baseview::MouseCursor::SResize, 108 | egui::CursorIcon::ResizeSouthWest => baseview::MouseCursor::SwResize, 109 | egui::CursorIcon::ResizeWest => baseview::MouseCursor::WResize, 110 | egui::CursorIcon::ResizeNorthWest => baseview::MouseCursor::NwResize, 111 | egui::CursorIcon::ResizeNorth => baseview::MouseCursor::NResize, 112 | egui::CursorIcon::ResizeNorthEast => baseview::MouseCursor::NeResize, 113 | egui::CursorIcon::ResizeColumn => baseview::MouseCursor::ColResize, 114 | egui::CursorIcon::ResizeRow => baseview::MouseCursor::RowResize, 115 | egui::CursorIcon::ZoomIn => baseview::MouseCursor::ZoomIn, 116 | egui::CursorIcon::ZoomOut => baseview::MouseCursor::ZoomOut, 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use baseview::{ 4 | Event, EventStatus, PhySize, Window, WindowHandle, WindowHandler, WindowOpenOptions, 5 | WindowScalePolicy, 6 | }; 7 | use copypasta::ClipboardProvider; 8 | use egui::{pos2, vec2, Pos2, Rect, Rgba, ViewportCommand}; 9 | use keyboard_types::Modifiers; 10 | use raw_window_handle::HasRawWindowHandle; 11 | 12 | use crate::{renderer::Renderer, GraphicsConfig}; 13 | 14 | pub struct Queue<'a> { 15 | bg_color: &'a mut Rgba, 16 | close_requested: &'a mut bool, 17 | physical_size: &'a mut PhySize, 18 | } 19 | 20 | impl<'a> Queue<'a> { 21 | pub(crate) fn new( 22 | bg_color: &'a mut Rgba, 23 | close_requested: &'a mut bool, 24 | physical_size: &'a mut PhySize, 25 | ) -> Self { 26 | Self { 27 | bg_color, 28 | //renderer, 29 | //repaint_requested, 30 | close_requested, 31 | physical_size, 32 | } 33 | } 34 | 35 | /// Set the background color. 36 | pub fn bg_color(&mut self, bg_color: Rgba) { 37 | *self.bg_color = bg_color; 38 | } 39 | 40 | /// Set size of the window. 41 | pub fn resize(&mut self, physical_size: PhySize) { 42 | *self.physical_size = physical_size; 43 | } 44 | 45 | /// Close the window. 46 | pub fn close_window(&mut self) { 47 | *self.close_requested = true; 48 | } 49 | } 50 | 51 | struct OpenSettings { 52 | scale_policy: WindowScalePolicy, 53 | logical_width: f64, 54 | logical_height: f64, 55 | title: String, 56 | } 57 | 58 | impl OpenSettings { 59 | fn new(settings: &WindowOpenOptions) -> Self { 60 | // WindowScalePolicy does not implement copy/clone. 61 | let scale_policy = match &settings.scale { 62 | WindowScalePolicy::SystemScaleFactor => WindowScalePolicy::SystemScaleFactor, 63 | WindowScalePolicy::ScaleFactor(scale) => WindowScalePolicy::ScaleFactor(*scale), 64 | }; 65 | 66 | Self { 67 | scale_policy, 68 | logical_width: settings.size.width, 69 | logical_height: settings.size.height, 70 | title: settings.title.clone(), 71 | } 72 | } 73 | } 74 | 75 | /// Handles an egui-baseview application 76 | pub struct EguiWindow 77 | where 78 | State: 'static + Send, 79 | U: FnMut(&egui::Context, &mut Queue, &mut State), 80 | U: 'static + Send, 81 | { 82 | user_state: Option, 83 | user_update: U, 84 | 85 | egui_ctx: egui::Context, 86 | viewport_id: egui::ViewportId, 87 | start_time: Instant, 88 | egui_input: egui::RawInput, 89 | pointer_pos_in_points: Option, 90 | current_cursor_icon: baseview::MouseCursor, 91 | 92 | renderer: Renderer, 93 | 94 | clipboard_ctx: Option, 95 | 96 | physical_size: PhySize, 97 | scale_policy: WindowScalePolicy, 98 | pixels_per_point: f32, 99 | points_per_pixel: f32, 100 | bg_color: Rgba, 101 | close_requested: bool, 102 | repaint_after: Option, 103 | } 104 | 105 | impl EguiWindow 106 | where 107 | State: 'static + Send, 108 | U: FnMut(&egui::Context, &mut Queue, &mut State), 109 | U: 'static + Send, 110 | { 111 | fn new( 112 | window: &mut baseview::Window<'_>, 113 | open_settings: OpenSettings, 114 | graphics_config: GraphicsConfig, 115 | mut build: B, 116 | update: U, 117 | mut state: State, 118 | ) -> EguiWindow 119 | where 120 | B: FnMut(&egui::Context, &mut Queue, &mut State), 121 | B: 'static + Send, 122 | { 123 | let renderer = Renderer::new(window, graphics_config).unwrap_or_else(|err| { 124 | // TODO: better error log and not panicking, but that's gonna require baseview changes 125 | log::error!("oops! the gpu backend couldn't initialize! \n {err}"); 126 | panic!("gpu backend failed to initialize: \n {err}") 127 | }); 128 | let egui_ctx = egui::Context::default(); 129 | 130 | // Assume scale for now until there is an event with a new one. 131 | let pixels_per_point = match open_settings.scale_policy { 132 | WindowScalePolicy::ScaleFactor(scale) => scale, 133 | WindowScalePolicy::SystemScaleFactor => 1.0, 134 | } as f32; 135 | let points_per_pixel = pixels_per_point.recip(); 136 | 137 | let screen_rect = Rect::from_min_size( 138 | Pos2::new(0f32, 0f32), 139 | vec2( 140 | open_settings.logical_width as f32, 141 | open_settings.logical_height as f32, 142 | ), 143 | ); 144 | 145 | let viewport_info = egui::ViewportInfo { 146 | parent: None, 147 | title: Some(open_settings.title), 148 | native_pixels_per_point: Some(pixels_per_point), 149 | focused: Some(true), 150 | inner_rect: Some(screen_rect), 151 | ..Default::default() 152 | }; 153 | let viewport_id = egui::ViewportId::default(); 154 | 155 | let mut egui_input = egui::RawInput { 156 | max_texture_side: Some(renderer.max_texture_side()), 157 | screen_rect: Some(screen_rect), 158 | ..Default::default() 159 | }; 160 | let _ = egui_input.viewports.insert(viewport_id, viewport_info); 161 | 162 | let mut physical_size = PhySize { 163 | width: (open_settings.logical_width * pixels_per_point as f64).round() as u32, 164 | height: (open_settings.logical_height * pixels_per_point as f64).round() as u32, 165 | }; 166 | 167 | let mut bg_color = Rgba::BLACK; 168 | let mut close_requested = false; 169 | let mut queue = Queue::new(&mut bg_color, &mut close_requested, &mut physical_size); 170 | (build)(&egui_ctx, &mut queue, &mut state); 171 | 172 | let clipboard_ctx = match copypasta::ClipboardContext::new() { 173 | Ok(clipboard_ctx) => Some(clipboard_ctx), 174 | Err(e) => { 175 | log::error!("Failed to initialize clipboard: {}", e); 176 | None 177 | } 178 | }; 179 | 180 | let start_time = Instant::now(); 181 | 182 | Self { 183 | user_state: Some(state), 184 | user_update: update, 185 | 186 | egui_ctx, 187 | viewport_id, 188 | start_time, 189 | egui_input, 190 | pointer_pos_in_points: None, 191 | current_cursor_icon: baseview::MouseCursor::Default, 192 | 193 | renderer, 194 | 195 | clipboard_ctx, 196 | 197 | physical_size, 198 | pixels_per_point, 199 | points_per_pixel, 200 | scale_policy: open_settings.scale_policy, 201 | bg_color, 202 | close_requested, 203 | repaint_after: Some(start_time), 204 | } 205 | } 206 | 207 | /// Open a new child window. 208 | /// 209 | /// * `parent` - The parent window. 210 | /// * `settings` - The settings of the window. 211 | /// * `state` - The initial state of your application. 212 | /// * `build` - Called once before the first frame. Allows you to do setup code and to 213 | /// call `ctx.set_fonts()`. Optional. 214 | /// * `update` - Called before each frame. Here you should update the state of your 215 | /// application and build the UI. 216 | pub fn open_parented( 217 | parent: &P, 218 | #[allow(unused_mut)] mut settings: WindowOpenOptions, 219 | graphics_config: GraphicsConfig, 220 | state: State, 221 | build: B, 222 | update: U, 223 | ) -> WindowHandle 224 | where 225 | P: HasRawWindowHandle, 226 | B: FnMut(&egui::Context, &mut Queue, &mut State), 227 | B: 'static + Send, 228 | { 229 | #[cfg(feature = "opengl")] 230 | if settings.gl_config.is_none() { 231 | settings.gl_config = Some(Default::default()); 232 | } 233 | 234 | let open_settings = OpenSettings::new(&settings); 235 | 236 | Window::open_parented( 237 | parent, 238 | settings, 239 | move |window: &mut baseview::Window<'_>| -> EguiWindow { 240 | EguiWindow::new(window, open_settings, graphics_config, build, update, state) 241 | }, 242 | ) 243 | } 244 | 245 | /// Open a new window that blocks the current thread until the window is destroyed. 246 | /// 247 | /// * `settings` - The settings of the window. 248 | /// * `state` - The initial state of your application. 249 | /// * `build` - Called once before the first frame. Allows you to do setup code and to 250 | /// call `ctx.set_fonts()`. Optional. 251 | /// * `update` - Called before each frame. Here you should update the state of your 252 | /// application and build the UI. 253 | pub fn open_blocking( 254 | #[allow(unused_mut)] mut settings: WindowOpenOptions, 255 | graphics_config: GraphicsConfig, 256 | state: State, 257 | build: B, 258 | update: U, 259 | ) where 260 | B: FnMut(&egui::Context, &mut Queue, &mut State), 261 | B: 'static + Send, 262 | { 263 | #[cfg(feature = "opengl")] 264 | if settings.gl_config.is_none() { 265 | settings.gl_config = Some(Default::default()); 266 | } 267 | 268 | let open_settings = OpenSettings::new(&settings); 269 | 270 | Window::open_blocking( 271 | settings, 272 | move |window: &mut baseview::Window<'_>| -> EguiWindow { 273 | EguiWindow::new(window, open_settings, graphics_config, build, update, state) 274 | }, 275 | ) 276 | } 277 | 278 | /// Update the pressed key modifiers when a mouse event has sent a new set of modifiers. 279 | fn update_modifiers(&mut self, modifiers: &Modifiers) { 280 | self.egui_input.modifiers.alt = !(*modifiers & Modifiers::ALT).is_empty(); 281 | self.egui_input.modifiers.shift = !(*modifiers & Modifiers::SHIFT).is_empty(); 282 | self.egui_input.modifiers.command = !(*modifiers & Modifiers::CONTROL).is_empty(); 283 | } 284 | } 285 | 286 | impl WindowHandler for EguiWindow 287 | where 288 | State: 'static + Send, 289 | U: FnMut(&egui::Context, &mut Queue, &mut State), 290 | U: 'static + Send, 291 | { 292 | fn on_frame(&mut self, window: &mut Window) { 293 | let Some(state) = &mut self.user_state else { 294 | return; 295 | }; 296 | 297 | self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64()); 298 | self.egui_input.screen_rect = Some(calculate_screen_rect( 299 | self.physical_size, 300 | self.points_per_pixel, 301 | )); 302 | 303 | self.egui_ctx.begin_pass(self.egui_input.take()); 304 | 305 | //let mut repaint_requested = false; 306 | let mut queue = Queue::new( 307 | &mut self.bg_color, 308 | &mut self.close_requested, 309 | &mut self.physical_size, 310 | ); 311 | 312 | (self.user_update)(&self.egui_ctx, &mut queue, state); 313 | 314 | if self.close_requested { 315 | window.close(); 316 | } 317 | 318 | // Prevent data from being allocated every frame by storing this 319 | // in a member field. 320 | let mut full_output = self.egui_ctx.end_pass(); 321 | 322 | let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else { 323 | // The main window was closed by egui. 324 | window.close(); 325 | return; 326 | }; 327 | 328 | for command in viewport_output.commands.iter() { 329 | match command { 330 | ViewportCommand::Close => { 331 | window.close(); 332 | } 333 | ViewportCommand::InnerSize(size) => window.resize(baseview::Size { 334 | width: size.x.max(1.0) as f64, 335 | height: size.y.max(1.0) as f64, 336 | }), 337 | _ => {} 338 | } 339 | } 340 | 341 | let now = Instant::now(); 342 | let do_repaint_now = if let Some(t) = self.repaint_after { 343 | now >= t || viewport_output.repaint_delay.is_zero() 344 | } else { 345 | viewport_output.repaint_delay.is_zero() 346 | }; 347 | 348 | if do_repaint_now { 349 | self.renderer.render( 350 | #[cfg(feature = "opengl")] 351 | window, 352 | self.bg_color, 353 | self.physical_size, 354 | self.pixels_per_point, 355 | &mut self.egui_ctx, 356 | &mut full_output, 357 | ); 358 | 359 | self.repaint_after = None; 360 | } else if let Some(repaint_after) = now.checked_add(viewport_output.repaint_delay) { 361 | // Schedule to repaint after the requested time has elapsed. 362 | self.repaint_after = Some(repaint_after); 363 | } 364 | 365 | for command in full_output.platform_output.commands { 366 | match command { 367 | egui::OutputCommand::CopyText(text) => { 368 | if let Some(clipboard_ctx) = &mut self.clipboard_ctx { 369 | if let Err(err) = clipboard_ctx.set_contents(text) { 370 | log::error!("Copy/Cut error: {}", err); 371 | } 372 | } 373 | } 374 | egui::OutputCommand::CopyImage(_) => { 375 | log::warn!("Copying images is not supported in egui_baseview."); 376 | } 377 | egui::OutputCommand::OpenUrl(open_url) => { 378 | if let Err(err) = open::that_detached(&open_url.url) { 379 | log::error!("Open error: {}", err); 380 | } 381 | } 382 | } 383 | } 384 | 385 | let cursor_icon = 386 | crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon); 387 | if self.current_cursor_icon != cursor_icon { 388 | self.current_cursor_icon = cursor_icon; 389 | 390 | // TODO: Set mouse cursor for MacOS once baseview supports it. 391 | #[cfg(not(target_os = "macos"))] 392 | window.set_mouse_cursor(cursor_icon); 393 | } 394 | 395 | // A temporary workaround for keyboard input not working sometimes in Windows. 396 | // See https://github.com/BillyDM/egui-baseview/issues/20 397 | #[cfg(feature = "windows_keyboard_workaround")] 398 | { 399 | #[cfg(target_os = "windows")] 400 | { 401 | if !full_output.platform_output.events.is_empty() 402 | || full_output.platform_output.ime.is_some() 403 | { 404 | window.focus(); 405 | } 406 | } 407 | } 408 | } 409 | 410 | fn on_event(&mut self, _window: &mut Window, event: Event) -> EventStatus { 411 | match &event { 412 | baseview::Event::Mouse(event) => match event { 413 | baseview::MouseEvent::CursorMoved { 414 | position, 415 | modifiers, 416 | } => { 417 | self.update_modifiers(modifiers); 418 | 419 | let pos = pos2(position.x as f32, position.y as f32); 420 | self.pointer_pos_in_points = Some(pos); 421 | self.egui_input.events.push(egui::Event::PointerMoved(pos)); 422 | } 423 | baseview::MouseEvent::ButtonPressed { button, modifiers } => { 424 | self.update_modifiers(modifiers); 425 | 426 | if let Some(pos) = self.pointer_pos_in_points { 427 | if let Some(button) = crate::translate::translate_mouse_button(*button) { 428 | self.egui_input.events.push(egui::Event::PointerButton { 429 | pos, 430 | button, 431 | pressed: true, 432 | modifiers: self.egui_input.modifiers, 433 | }); 434 | } 435 | } 436 | } 437 | baseview::MouseEvent::ButtonReleased { button, modifiers } => { 438 | self.update_modifiers(modifiers); 439 | 440 | if let Some(pos) = self.pointer_pos_in_points { 441 | if let Some(button) = crate::translate::translate_mouse_button(*button) { 442 | self.egui_input.events.push(egui::Event::PointerButton { 443 | pos, 444 | button, 445 | pressed: false, 446 | modifiers: self.egui_input.modifiers, 447 | }); 448 | } 449 | } 450 | } 451 | baseview::MouseEvent::WheelScrolled { 452 | delta: scroll_delta, 453 | modifiers, 454 | } => { 455 | self.update_modifiers(modifiers); 456 | 457 | #[allow(unused_mut)] 458 | let (unit, mut delta) = match scroll_delta { 459 | baseview::ScrollDelta::Lines { x, y } => { 460 | (egui::MouseWheelUnit::Line, egui::vec2(*x, *y)) 461 | } 462 | 463 | baseview::ScrollDelta::Pixels { x, y } => ( 464 | egui::MouseWheelUnit::Point, 465 | egui::vec2(*x, *y) * self.points_per_pixel, 466 | ), 467 | }; 468 | 469 | if cfg!(target_os = "macos") { 470 | // This is still buggy in winit despite 471 | // https://github.com/rust-windowing/winit/issues/1695 being closed 472 | // 473 | // TODO: See if this is an issue in baseview as well. 474 | delta.x *= -1.0; 475 | } 476 | 477 | self.egui_input.events.push(egui::Event::MouseWheel { 478 | unit, 479 | delta, 480 | modifiers: self.egui_input.modifiers, 481 | }); 482 | } 483 | baseview::MouseEvent::CursorLeft => { 484 | self.pointer_pos_in_points = None; 485 | self.egui_input.events.push(egui::Event::PointerGone); 486 | } 487 | _ => {} 488 | }, 489 | baseview::Event::Keyboard(event) => { 490 | use keyboard_types::Code; 491 | 492 | let pressed = event.state == keyboard_types::KeyState::Down; 493 | 494 | match event.code { 495 | Code::ShiftLeft | Code::ShiftRight => self.egui_input.modifiers.shift = pressed, 496 | Code::ControlLeft | Code::ControlRight => { 497 | self.egui_input.modifiers.ctrl = pressed; 498 | 499 | #[cfg(not(target_os = "macos"))] 500 | { 501 | self.egui_input.modifiers.command = pressed; 502 | } 503 | } 504 | Code::AltLeft | Code::AltRight => self.egui_input.modifiers.alt = pressed, 505 | Code::MetaLeft | Code::MetaRight => { 506 | #[cfg(target_os = "macos")] 507 | { 508 | self.egui_input.modifiers.mac_cmd = pressed; 509 | self.egui_input.modifiers.command = pressed; 510 | } 511 | // prevent `rustfmt` from breaking this 512 | } 513 | _ => (), 514 | } 515 | 516 | if let Some(key) = crate::translate::translate_virtual_key(&event.key) { 517 | self.egui_input.events.push(egui::Event::Key { 518 | key, 519 | physical_key: None, 520 | pressed, 521 | repeat: event.repeat, 522 | modifiers: self.egui_input.modifiers, 523 | }); 524 | } 525 | 526 | if pressed { 527 | // VirtualKeyCode::Paste etc in winit are broken/untrustworthy, 528 | // so we detect these things manually: 529 | // 530 | // TODO: See if this is an issue in baseview as well. 531 | if is_cut_command(self.egui_input.modifiers, event.code) { 532 | self.egui_input.events.push(egui::Event::Cut); 533 | } else if is_copy_command(self.egui_input.modifiers, event.code) { 534 | self.egui_input.events.push(egui::Event::Copy); 535 | } else if is_paste_command(self.egui_input.modifiers, event.code) { 536 | if let Some(clipboard_ctx) = &mut self.clipboard_ctx { 537 | match clipboard_ctx.get_contents() { 538 | Ok(contents) => { 539 | self.egui_input.events.push(egui::Event::Text(contents)) 540 | } 541 | Err(err) => { 542 | log::error!("Paste error: {}", err); 543 | } 544 | } 545 | } 546 | } else if let keyboard_types::Key::Character(written) = &event.key { 547 | if !self.egui_input.modifiers.ctrl && !self.egui_input.modifiers.command { 548 | self.egui_input 549 | .events 550 | .push(egui::Event::Text(written.clone())); 551 | } 552 | } 553 | } 554 | } 555 | baseview::Event::Window(event) => match event { 556 | baseview::WindowEvent::Resized(window_info) => { 557 | self.pixels_per_point = match self.scale_policy { 558 | WindowScalePolicy::ScaleFactor(scale) => scale, 559 | WindowScalePolicy::SystemScaleFactor => window_info.scale(), 560 | } as f32; 561 | self.points_per_pixel = self.pixels_per_point.recip(); 562 | 563 | self.physical_size = window_info.physical_size(); 564 | 565 | let screen_rect = 566 | calculate_screen_rect(self.physical_size, self.points_per_pixel); 567 | 568 | self.egui_input.screen_rect = Some(screen_rect); 569 | 570 | let viewport_info = self 571 | .egui_input 572 | .viewports 573 | .get_mut(&self.viewport_id) 574 | .unwrap(); 575 | viewport_info.native_pixels_per_point = Some(self.pixels_per_point); 576 | viewport_info.inner_rect = Some(screen_rect); 577 | 578 | // Schedule to repaint on the next frame. 579 | self.repaint_after = Some(Instant::now()); 580 | } 581 | baseview::WindowEvent::Focused => { 582 | self.egui_input 583 | .events 584 | .push(egui::Event::WindowFocused(true)); 585 | self.egui_input 586 | .viewports 587 | .get_mut(&self.viewport_id) 588 | .unwrap() 589 | .focused = Some(true); 590 | } 591 | baseview::WindowEvent::Unfocused => { 592 | self.egui_input 593 | .events 594 | .push(egui::Event::WindowFocused(false)); 595 | self.egui_input 596 | .viewports 597 | .get_mut(&self.viewport_id) 598 | .unwrap() 599 | .focused = Some(false); 600 | } 601 | baseview::WindowEvent::WillClose => {} 602 | }, 603 | } 604 | 605 | EventStatus::Captured 606 | } 607 | } 608 | 609 | fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool { 610 | (modifiers.command && keycode == keyboard_types::Code::KeyX) 611 | || (cfg!(target_os = "windows") 612 | && modifiers.shift 613 | && keycode == keyboard_types::Code::Delete) 614 | } 615 | 616 | fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool { 617 | (modifiers.command && keycode == keyboard_types::Code::KeyC) 618 | || (cfg!(target_os = "windows") 619 | && modifiers.ctrl 620 | && keycode == keyboard_types::Code::Insert) 621 | } 622 | 623 | fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool { 624 | (modifiers.command && keycode == keyboard_types::Code::KeyV) 625 | || (cfg!(target_os = "windows") 626 | && modifiers.shift 627 | && keycode == keyboard_types::Code::Insert) 628 | } 629 | 630 | /// Calculate screen rectangle in logical size. 631 | fn calculate_screen_rect(physical_size: PhySize, points_per_pixel: f32) -> Rect { 632 | let logical_size = ( 633 | physical_size.width as f32 * points_per_pixel, 634 | physical_size.height as f32 * points_per_pixel, 635 | ); 636 | Rect::from_min_size(Pos2::new(0f32, 0f32), vec2(logical_size.0, logical_size.1)) 637 | } 638 | --------------------------------------------------------------------------------