├── .github └── workflows │ └── nannou_egui.yml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.toml ├── LICENSE ├── LICENSE-APACHE ├── README.md ├── nannou_egui ├── Cargo.toml ├── examples │ ├── circle_packing.rs │ └── tune_color.rs ├── media │ ├── circle_packing.gif │ └── tune_egui.gif └── src │ └── lib.rs └── nannou_egui_demo_app ├── Cargo.toml └── src └── main.rs /.github/workflows/nannou_egui.yml: -------------------------------------------------------------------------------- 1 | name: nannou_egui 2 | on: [push, pull_request] 3 | jobs: 4 | cargo-fmt-check: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install stable 9 | uses: actions-rs/toolchain@v1 10 | with: 11 | profile: minimal 12 | toolchain: stable 13 | override: true 14 | components: rustfmt 15 | - name: cargo fmt check 16 | uses: actions-rs/cargo@v1 17 | with: 18 | command: fmt 19 | args: --all -- --check 20 | 21 | cargo-test: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Update apt 26 | run: sudo apt update 27 | - name: Install alsa dev tools 28 | run: sudo apt-get install libasound2-dev 29 | - name: Install libxcb dev tools 30 | run: sudo apt-get install libxcb-composite0-dev 31 | - name: Install stable 32 | uses: actions-rs/toolchain@v1 33 | with: 34 | profile: minimal 35 | toolchain: stable 36 | override: true 37 | - name: cargo test 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | args: --all --verbose 42 | 43 | cargo-doc: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v2 47 | - name: Update apt 48 | run: sudo apt update 49 | - name: Install stable 50 | uses: actions-rs/toolchain@v1 51 | with: 52 | profile: minimal 53 | toolchain: stable 54 | override: true 55 | - name: cargo doc 56 | uses: actions-rs/cargo@v1 57 | with: 58 | command: doc 59 | args: --all --verbose 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "cppvsdbg", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/target/debug/nannou_egui_example.exe", 12 | "args": [], 13 | "stopAtEntry": false, 14 | "cwd": "${workspaceFolder}", 15 | "environment": [], 16 | "externalConsole": false, 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "nannou_egui", 4 | "nannou_egui_demo_app", 5 | ] 6 | 7 | # wgpu requires feature resolver 2. 8 | resolver = "2" 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alexandru Ene 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 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Alexandru Ene 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nannou_egui 2 | [![Actions Status](https://github.com/alexene/nannou_egui/workflows/nannou_egui/badge.svg)](https://github.com/alexene/nannou_egui/actions) [![Crates.io](https://img.shields.io/crates/v/nannou_egui.svg)](https://crates.io/crates/nannou_egui) [![Crates.io](https://img.shields.io/crates/l/nannou_egui.svg)](https://github.com/alexene/nannou_egui/blob/master/LICENSE-MIT) [![docs.rs](https://docs.rs/nannou_egui/badge.svg)](https://docs.rs/nannou_egui/) 3 | 4 | ### NOTE: This project has been moved upstream to [the nannou repository](https://github.com/nannou-org/nannou). 5 | -------------------------------------------------------------------------------- /nannou_egui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nannou_egui" 3 | version = "0.5.0" 4 | authors = ["Alexandru Ene "] 5 | edition = "2018" 6 | license = "MIT/Apache-2.0" 7 | description = "egui integration for nannou" 8 | repository = "https://github.com/AlexEne/nannou_egui" 9 | readme = "../README.md" 10 | 11 | [dependencies] 12 | egui_wgpu_backend = "0.14" 13 | egui = "0.15.0" 14 | winit = "0.25" 15 | nannou = "0.18.1" 16 | -------------------------------------------------------------------------------- /nannou_egui/examples/circle_packing.rs: -------------------------------------------------------------------------------- 1 | use nannou::{color::rgb_u32, rand::thread_rng}; 2 | use nannou::{prelude::*, rand::prelude::SliceRandom}; 3 | use nannou_egui::{self, egui, Egui}; 4 | 5 | const WIDTH: f32 = 640.0; 6 | const HEIGHT: f32 = 360.0; 7 | 8 | fn main() { 9 | nannou::app(model).update(update).run(); 10 | } 11 | 12 | struct Circle { 13 | x: f32, 14 | y: f32, 15 | radius: f32, 16 | color: Hsv, 17 | } 18 | 19 | struct Settings { 20 | min_radius: f32, 21 | max_radius: f32, 22 | circle_count: usize, 23 | } 24 | 25 | struct Model { 26 | circles: Vec, 27 | settings: Settings, 28 | egui: Egui, 29 | } 30 | 31 | fn model(app: &App) -> Model { 32 | let window_id = app 33 | .new_window() 34 | .size(WIDTH as u32, HEIGHT as u32) 35 | .view(view) 36 | .raw_event(raw_window_event) 37 | .build() 38 | .unwrap(); 39 | 40 | let window = app.window(window_id).unwrap(); 41 | let egui = Egui::from_window(&window); 42 | Model { 43 | circles: Vec::new(), 44 | egui, 45 | settings: Settings { 46 | min_radius: 10.0, 47 | max_radius: 100.0, 48 | circle_count: 10, 49 | }, 50 | } 51 | } 52 | 53 | fn update(_app: &App, model: &mut Model, update: Update) { 54 | let Model { 55 | ref mut egui, 56 | ref mut settings, 57 | ref mut circles, 58 | .. 59 | } = *model; 60 | 61 | egui.set_elapsed_time(update.since_start); 62 | let ctx = egui.begin_frame(); 63 | egui::Window::new("Workshop window").show(&ctx, |ui| { 64 | let mut changed = false; 65 | changed |= ui 66 | .add(egui::Slider::new(&mut settings.min_radius, 0.0..=20.0).text("min radius")) 67 | .changed(); 68 | changed |= ui 69 | .add(egui::Slider::new(&mut settings.max_radius, 0.0..=200.0).text("max radius")) 70 | .changed(); 71 | changed |= ui 72 | .add(egui::Slider::new(&mut settings.circle_count, 0..=2000).text("circle count")) 73 | .changed(); 74 | changed |= ui.button("Generate").clicked(); 75 | if changed { 76 | *circles = generate_circles(settings); 77 | } 78 | }); 79 | } 80 | 81 | fn raw_window_event(_app: &App, model: &mut Model, event: &nannou::winit::event::WindowEvent) { 82 | model.egui.handle_raw_event(event); 83 | } 84 | 85 | fn view(app: &App, model: &Model, frame: Frame) { 86 | let draw = app.draw(); 87 | 88 | draw.background().color(BLACK); 89 | 90 | for circle in model.circles.iter() { 91 | draw.ellipse() 92 | .x_y(circle.x, circle.y) 93 | .radius(circle.radius) 94 | .color(circle.color); 95 | } 96 | 97 | draw.to_frame(app, &frame).unwrap(); 98 | 99 | model.egui.draw_to_frame(&frame); 100 | } 101 | 102 | fn intersects(circle: &Circle, circles: &Vec) -> bool { 103 | for other in circles.iter() { 104 | let dist: f32 = 105 | ((other.x - circle.x).pow(2) as f32 + (other.y - circle.y).pow(2) as f32).sqrt(); 106 | if dist < circle.radius + other.radius { 107 | return true; 108 | } 109 | } 110 | false 111 | } 112 | 113 | fn generate_circles(settings: &mut Settings) -> Vec { 114 | let colors = [ 115 | hsv_from_hex_rgb(0x264653), 116 | hsv_from_hex_rgb(0x2a9d8f), 117 | hsv_from_hex_rgb(0xe9c46a), 118 | hsv_from_hex_rgb(0xf4a261), 119 | hsv_from_hex_rgb(0xe76f51), 120 | ]; 121 | 122 | let mut circles = Vec::new(); 123 | 124 | let mut rng = thread_rng(); 125 | 126 | let mut loops = 0; 127 | loop { 128 | let x = random_range(-WIDTH / 2.0, WIDTH / 2.0); 129 | let y = random_range(-HEIGHT / 2.0, HEIGHT / 2.0); 130 | let radius = random_range(settings.min_radius, settings.max_radius); 131 | let color = *colors.choose(&mut rng).unwrap(); 132 | let mut circle = Circle { 133 | x, 134 | y, 135 | radius, 136 | color, 137 | }; 138 | 139 | loops += 1; 140 | if loops > 20000 { 141 | break; 142 | } 143 | 144 | if intersects(&circle, &circles) { 145 | continue; 146 | } 147 | 148 | let mut prev_radius = circle.radius; 149 | while !intersects(&circle, &circles) { 150 | // Grow the circle 151 | prev_radius = circle.radius; 152 | circle.radius += 10.0; 153 | 154 | if circle.radius >= settings.max_radius { 155 | break; 156 | } 157 | } 158 | circle.radius = prev_radius; 159 | 160 | circles.push(circle); 161 | 162 | if circles.len() >= settings.circle_count { 163 | break; 164 | } 165 | } 166 | 167 | circles 168 | } 169 | 170 | fn hsv_from_hex_rgb(color: u32) -> Hsv { 171 | let color = rgb_u32(color); 172 | rgba( 173 | color.red as f32 / 255.0, 174 | color.green as f32 / 255.0, 175 | color.blue as f32 / 255.0, 176 | 1.0, 177 | ) 178 | .into() 179 | } 180 | -------------------------------------------------------------------------------- /nannou_egui/examples/tune_color.rs: -------------------------------------------------------------------------------- 1 | use nannou::prelude::*; 2 | use nannou_egui::{egui, Egui}; 3 | 4 | const WIDTH: f32 = 640.0; 5 | const HEIGHT: f32 = 360.0; 6 | 7 | fn main() { 8 | nannou::app(model).update(update).run(); 9 | } 10 | 11 | struct Model { 12 | egui: Egui, 13 | radius: f32, 14 | color: Hsv, 15 | } 16 | 17 | fn model(app: &App) -> Model { 18 | // Create a new window! Store the ID so we can refer to it later. 19 | let window_id = app 20 | .new_window() 21 | .title("Nannou + Egui") 22 | .size(WIDTH as u32, HEIGHT as u32) 23 | .raw_event(raw_window_event) // This is where we forward all raw events for egui to process them 24 | .view(view) // The function that will be called for presenting graphics to a frame. 25 | .build() 26 | .unwrap(); 27 | 28 | let window = app.window(window_id).unwrap(); 29 | 30 | Model { 31 | egui: Egui::from_window(&window), 32 | radius: 40.0, 33 | color: hsv(10.0, 0.5, 1.0), 34 | } 35 | } 36 | 37 | fn update(_app: &App, model: &mut Model, update: Update) { 38 | let Model { 39 | ref mut egui, 40 | ref mut radius, 41 | ref mut color, 42 | } = *model; 43 | 44 | egui.set_elapsed_time(update.since_start); 45 | let ctx = egui.begin_frame(); 46 | egui::Window::new("EGUI window") 47 | .default_size(egui::vec2(0.0, 200.0)) 48 | .show(&ctx, |ui| { 49 | ui.separator(); 50 | ui.label("Tune parameters with ease"); 51 | ui.add(egui::Slider::new(radius, 10.0..=100.0).text("Radius")); 52 | nannou_egui::edit_color(ui, color); 53 | }); 54 | } 55 | 56 | fn raw_window_event(_app: &App, model: &mut Model, event: &nannou::winit::event::WindowEvent) { 57 | model.egui.handle_raw_event(event); 58 | } 59 | 60 | // Draw the state of your `Model` into the given `Frame` here. 61 | fn view(app: &App, model: &Model, frame: Frame) { 62 | let draw = app.draw(); 63 | 64 | frame.clear(BLACK); 65 | 66 | draw.ellipse() 67 | .x_y(100.0, 100.0) 68 | .radius(model.radius) 69 | .color(model.color); 70 | 71 | draw.to_frame(app, &frame).unwrap(); 72 | 73 | // Do this as the last operation on your frame. 74 | model.egui.draw_to_frame(&frame); 75 | } 76 | -------------------------------------------------------------------------------- /nannou_egui/media/circle_packing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexEne/nannou_egui/2f7ad8165596474193ea8617badf9894a1b2c830/nannou_egui/media/circle_packing.gif -------------------------------------------------------------------------------- /nannou_egui/media/tune_egui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexEne/nannou_egui/2f7ad8165596474193ea8617badf9894a1b2c830/nannou_egui/media/tune_egui.gif -------------------------------------------------------------------------------- /nannou_egui/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use egui; 2 | pub use egui::color_picker; 3 | pub use egui_wgpu_backend; 4 | 5 | use egui::{pos2, ClippedMesh, CtxRef}; 6 | use egui_wgpu_backend::{epi, ScreenDescriptor}; 7 | use nannou::{wgpu, winit::event::VirtualKeyCode, winit::event::WindowEvent::*}; 8 | use std::{ 9 | cell::RefCell, 10 | ops::Deref, 11 | sync::{Arc, Mutex}, 12 | time::Duration, 13 | }; 14 | 15 | /// All `egui`-related state for a single window. 16 | /// 17 | /// Includes the context, a renderer, and an input tracker. 18 | /// 19 | /// For multi-window user interfaces, you will need to create an instance of this type per-window. 20 | pub struct Egui { 21 | context: CtxRef, 22 | renderer: RefCell, 23 | input: Input, 24 | } 25 | 26 | /// A wrapper around all necessary state for rendering a `Egui` to a single texture (often a window 27 | /// texture). 28 | /// 29 | /// For targeting more than one window, users should construct a `Egui` for each. 30 | pub struct Renderer { 31 | render_pass: egui_wgpu_backend::RenderPass, 32 | paint_jobs: Vec, 33 | } 34 | 35 | /// Tracking user and application event input. 36 | pub struct Input { 37 | pub pointer_pos: egui::Pos2, 38 | pub raw: egui::RawInput, 39 | pub window_size_pixels: [u32; 2], 40 | pub window_scale_factor: f32, 41 | } 42 | 43 | /// A wrapper around a `CtxRef` on which `begin_frame` was called. 44 | /// 45 | /// Automatically calls `end_frame` on `drop` in the case that it wasn't already called by the 46 | /// usef. 47 | pub struct FrameCtx<'a> { 48 | ui: &'a mut Egui, 49 | ended: bool, 50 | } 51 | 52 | struct RepaintSignal(Mutex); 53 | 54 | impl Egui { 55 | /// Construct the `Egui` from its parts. 56 | /// 57 | /// The given `device` must be the same used to create the queue to which the Egui's render 58 | /// commands will be submitted. 59 | /// 60 | /// The `target_format`, `target_msaa_samples`, `window_scale_factor` and `window_size_pixels` 61 | /// must match the window to which the UI will be drawn. 62 | /// 63 | /// The `context` should have the desired initial styling and fonts already set. 64 | pub fn new( 65 | device: &wgpu::Device, 66 | target_format: wgpu::TextureFormat, 67 | target_msaa_samples: u32, 68 | window_scale_factor: f32, 69 | window_size_pixels: [u32; 2], 70 | ) -> Self { 71 | let renderer = RefCell::new(Renderer::new(device, target_format, target_msaa_samples)); 72 | let input = Input::new(window_scale_factor, window_size_pixels); 73 | let context = Default::default(); 74 | Self { 75 | renderer, 76 | input, 77 | context, 78 | } 79 | } 80 | 81 | /// Construct a `Egui` associated with the given window. 82 | pub fn from_window(window: &nannou::window::Window) -> Self { 83 | let device = window.device(); 84 | let format = nannou::Frame::TEXTURE_FORMAT; 85 | let msaa_samples = window.msaa_samples(); 86 | let scale_factor = window.scale_factor(); 87 | let (w_px, h_px) = window.inner_size_pixels(); 88 | Self::new(device, format, msaa_samples, scale_factor, [w_px, h_px]) 89 | } 90 | 91 | /// Access to the inner `egui::CtxRef`. 92 | pub fn ctx(&self) -> &CtxRef { 93 | &self.context 94 | } 95 | 96 | /// Access to the currently tracked input state. 97 | pub fn input(&self) -> &Input { 98 | &self.input 99 | } 100 | 101 | /// Handles a raw window event, tracking all input and events relevant to the UI as necessary. 102 | pub fn handle_raw_event(&mut self, event: &winit::event::WindowEvent) { 103 | self.input.handle_raw_event(event); 104 | } 105 | 106 | /// Set the elapsed time since the `Egui` app started running. 107 | pub fn set_elapsed_time(&mut self, elapsed: Duration) { 108 | self.input.set_elapsed_time(elapsed); 109 | } 110 | 111 | /// Begin describing a UI frame. 112 | pub fn begin_frame(&mut self) -> FrameCtx { 113 | self.begin_frame_inner(); 114 | let ui = self; 115 | let ended = false; 116 | FrameCtx { ui, ended } 117 | } 118 | 119 | /// Registers a wgpu::Texture with a egui::TextureId. 120 | pub fn texture_from_wgpu_texture( 121 | &mut self, 122 | device: &wgpu::Device, 123 | texture: &wgpu::Texture, 124 | texture_filter: wgpu::FilterMode, 125 | ) -> egui::TextureId { 126 | self.renderer 127 | .borrow_mut() 128 | .render_pass 129 | .egui_texture_from_wgpu_texture(device, texture, texture_filter) 130 | } 131 | 132 | /// Registers a wgpu::Texture with an existing egui::TextureId. 133 | pub fn update_texture_from_wgpu_texture( 134 | &mut self, 135 | device: &wgpu::Device, 136 | texture: &wgpu::Texture, 137 | texture_filter: wgpu::FilterMode, 138 | id: egui::TextureId, 139 | ) -> Result<(), egui_wgpu_backend::BackendError> { 140 | self.renderer 141 | .borrow_mut() 142 | .render_pass 143 | .update_egui_texture_from_wgpu_texture(device, texture, texture_filter, id) 144 | } 145 | 146 | /// Draws the contents of the inner `context` to the given frame. 147 | pub fn draw_to_frame( 148 | &self, 149 | frame: &nannou::Frame, 150 | ) -> Result<(), egui_wgpu_backend::BackendError> { 151 | let mut renderer = self.renderer.borrow_mut(); 152 | renderer.draw_to_frame(&self.context, frame) 153 | } 154 | 155 | /// Provide access to an `epi::Frame` within the given function. 156 | /// 157 | /// This method is primarily used for apps based on the `epi` interface. 158 | pub fn with_epi_frame(&mut self, proxy: nannou::app::Proxy, f: F) 159 | where 160 | F: FnOnce(&CtxRef, &mut epi::Frame), 161 | { 162 | let mut renderer = self.renderer.borrow_mut(); 163 | let integration_info = epi::IntegrationInfo { 164 | native_pixels_per_point: Some(self.input.window_scale_factor as _), 165 | // TODO: Provide access to this stuff. 166 | web_info: None, 167 | prefer_dark_mode: None, 168 | cpu_usage: None, 169 | name: "egui_nannou_wgpu", 170 | }; 171 | let mut app_output = epi::backend::AppOutput::default(); 172 | let repaint_signal = Arc::new(RepaintSignal(Mutex::new(proxy))); 173 | let mut frame = epi::backend::FrameBuilder { 174 | info: integration_info, 175 | tex_allocator: &mut renderer.render_pass, 176 | // TODO: We may want to support a http feature for hyperlinks? 177 | // #[cfg(feature = "http")] 178 | // http: http.clone(), 179 | output: &mut app_output, 180 | repaint_signal: repaint_signal as Arc<_>, 181 | } 182 | .build(); 183 | f(&self.context, &mut frame) 184 | } 185 | 186 | /// The same as `with_epi_frame`, but calls `begin_frame` before calling the given function, 187 | /// and then calls `end_frame` before returning. 188 | pub fn do_frame_with_epi_frame(&mut self, proxy: nannou::app::Proxy, f: F) -> egui::Output 189 | where 190 | F: FnOnce(&CtxRef, &mut epi::Frame), 191 | { 192 | self.begin_frame_inner(); 193 | self.with_epi_frame(proxy.clone(), f); 194 | let output = self.end_frame_inner(); 195 | 196 | // If a repaint is required, ensure the event loop emits another update. 197 | if output.needs_repaint { 198 | proxy.wakeup().unwrap(); 199 | } 200 | 201 | output 202 | } 203 | 204 | fn begin_frame_inner(&mut self) { 205 | self.context.begin_frame(self.input.raw.take()); 206 | } 207 | 208 | fn end_frame_inner(&mut self) -> egui::Output { 209 | let (output, paint_cmds) = self.context.end_frame(); 210 | self.renderer.borrow_mut().paint_jobs = self.context.tessellate(paint_cmds); 211 | output 212 | } 213 | } 214 | 215 | impl Input { 216 | /// Initialise user input and window event tracking with the given target scale factor and size 217 | /// in pixels. 218 | pub fn new(window_scale_factor: f32, window_size_pixels: [u32; 2]) -> Self { 219 | let raw = egui::RawInput { 220 | pixels_per_point: Some(window_scale_factor), 221 | ..Default::default() 222 | }; 223 | let pointer_pos = Default::default(); 224 | let mut input = Self { 225 | raw, 226 | pointer_pos, 227 | window_scale_factor, 228 | window_size_pixels, 229 | }; 230 | input.raw.screen_rect = Some(input.egui_window_rect()); 231 | input 232 | } 233 | 234 | /// Handles a raw window event, tracking all input and events relevant to the UI as necessary. 235 | pub fn handle_raw_event(&mut self, event: &winit::event::WindowEvent) { 236 | match event { 237 | Resized(physical_size) => { 238 | self.window_size_pixels = [physical_size.width, physical_size.height]; 239 | self.raw.screen_rect = Some(self.egui_window_rect()); 240 | } 241 | ScaleFactorChanged { 242 | scale_factor, 243 | new_inner_size, 244 | } => { 245 | self.window_scale_factor = *scale_factor as f32; 246 | self.window_size_pixels = [new_inner_size.width, new_inner_size.height]; 247 | self.raw.pixels_per_point = Some(self.window_scale_factor); 248 | self.raw.screen_rect = Some(self.egui_window_rect()); 249 | } 250 | MouseInput { state, button, .. } => { 251 | if let winit::event::MouseButton::Other(..) = button { 252 | } else { 253 | self.raw.events.push(egui::Event::PointerButton { 254 | pos: self.pointer_pos, 255 | button: match button { 256 | winit::event::MouseButton::Left => egui::PointerButton::Primary, 257 | winit::event::MouseButton::Right => egui::PointerButton::Secondary, 258 | winit::event::MouseButton::Middle => egui::PointerButton::Middle, 259 | winit::event::MouseButton::Other(_) => unreachable!(), 260 | }, 261 | pressed: *state == winit::event::ElementState::Pressed, 262 | modifiers: self.raw.modifiers, 263 | }); 264 | } 265 | } 266 | MouseWheel { delta, .. } => { 267 | match delta { 268 | winit::event::MouseScrollDelta::LineDelta(x, y) => { 269 | let line_height = 24.0; 270 | self.raw.scroll_delta = egui::vec2(*x, *y) * line_height; 271 | } 272 | winit::event::MouseScrollDelta::PixelDelta(delta) => { 273 | // Actually point delta 274 | self.raw.scroll_delta = egui::vec2(delta.x as f32, delta.y as f32); 275 | } 276 | } 277 | } 278 | CursorMoved { position, .. } => { 279 | self.pointer_pos = pos2( 280 | position.x as f32 / self.window_scale_factor as f32, 281 | position.y as f32 / self.window_scale_factor as f32, 282 | ); 283 | self.raw 284 | .events 285 | .push(egui::Event::PointerMoved(self.pointer_pos)); 286 | } 287 | CursorLeft { .. } => { 288 | self.raw.events.push(egui::Event::PointerGone); 289 | } 290 | ModifiersChanged(input) => { 291 | self.raw.modifiers = winit_to_egui_modifiers(*input); 292 | } 293 | KeyboardInput { input, .. } => { 294 | if let Some(virtual_keycode) = input.virtual_keycode { 295 | if let Some(key) = winit_to_egui_key_code(virtual_keycode) { 296 | // TODO figure out why if I enable this the characters get ignored 297 | self.raw.events.push(egui::Event::Key { 298 | key, 299 | pressed: input.state == winit::event::ElementState::Pressed, 300 | modifiers: self.raw.modifiers, 301 | }); 302 | } 303 | } 304 | } 305 | ReceivedCharacter(ch) => { 306 | if is_printable(*ch) && !self.raw.modifiers.ctrl && !self.raw.modifiers.command { 307 | self.raw.events.push(egui::Event::Text(ch.to_string())); 308 | } 309 | } 310 | _ => {} 311 | } 312 | } 313 | 314 | /// Set the elapsed time since the `Egui` app started running. 315 | pub fn set_elapsed_time(&mut self, elapsed: Duration) { 316 | self.raw.time = Some(elapsed.as_secs_f64()); 317 | } 318 | 319 | /// Small helper for the common task of producing an `egui::Rect` describing the window. 320 | fn egui_window_rect(&self) -> egui::Rect { 321 | let [w, h] = self.window_size_pixels; 322 | egui::Rect::from_min_size( 323 | Default::default(), 324 | egui::vec2(w as f32, h as f32) / self.window_scale_factor as f32, 325 | ) 326 | } 327 | } 328 | 329 | impl Renderer { 330 | /// Create a new `Renderer` from its parts. 331 | /// 332 | /// The `device` must be the same that was used to create the queue to which the `Renderer`s 333 | /// render passes will be submitted. 334 | /// 335 | /// The `target_format` and `target_msaa_samples` should describe the target texture to which 336 | /// the `Egui` will be rendered. 337 | pub fn new( 338 | device: &wgpu::Device, 339 | target_format: wgpu::TextureFormat, 340 | target_msaa_samples: u32, 341 | ) -> Self { 342 | Self { 343 | render_pass: egui_wgpu_backend::RenderPass::new( 344 | device, 345 | target_format, 346 | target_msaa_samples, 347 | ), 348 | paint_jobs: Vec::new(), 349 | } 350 | } 351 | 352 | /// Construct a `Renderer` ready for drawing to the given window. 353 | pub fn from_window(window: &nannou::window::Window) -> Self { 354 | let device = window.device(); 355 | let format = nannou::Frame::TEXTURE_FORMAT; 356 | let msaa_samples = window.msaa_samples(); 357 | Self::new(device, format, msaa_samples) 358 | } 359 | 360 | /// Encode a render pass for drawing the given context's texture to the given `dst_texture`. 361 | pub fn encode_render_pass( 362 | &mut self, 363 | context: &CtxRef, 364 | device: &wgpu::Device, 365 | queue: &wgpu::Queue, 366 | encoder: &mut wgpu::CommandEncoder, 367 | dst_size_pixels: [u32; 2], 368 | dst_scale_factor: f32, 369 | dst_texture: &wgpu::TextureView, 370 | ) -> Result<(), egui_wgpu_backend::BackendError> { 371 | let render_pass = &mut self.render_pass; 372 | let paint_jobs = &self.paint_jobs; 373 | let [physical_width, physical_height] = dst_size_pixels; 374 | let screen_descriptor = ScreenDescriptor { 375 | physical_width, 376 | physical_height, 377 | scale_factor: dst_scale_factor, 378 | }; 379 | render_pass.update_texture(device, queue, &*context.texture()); 380 | render_pass.update_user_textures(&device, &queue); 381 | render_pass.update_buffers(device, queue, &paint_jobs, &screen_descriptor); 382 | render_pass.execute(encoder, dst_texture, &paint_jobs, &screen_descriptor, None) 383 | } 384 | 385 | /// Encodes a render pass for drawing the given context's texture to the given frame. 386 | pub fn draw_to_frame( 387 | &mut self, 388 | context: &CtxRef, 389 | frame: &nannou::Frame, 390 | ) -> Result<(), egui_wgpu_backend::BackendError> { 391 | let device_queue_pair = frame.device_queue_pair(); 392 | let device = device_queue_pair.device(); 393 | let queue = device_queue_pair.queue(); 394 | let size_pixels = frame.texture_size(); 395 | let [width_px, _] = size_pixels; 396 | let scale_factor = width_px as f32 / frame.rect().w(); 397 | let texture_view = frame.texture_view(); 398 | let mut encoder = frame.command_encoder(); 399 | self.encode_render_pass( 400 | context, 401 | device, 402 | queue, 403 | &mut encoder, 404 | size_pixels, 405 | scale_factor, 406 | texture_view, 407 | ) 408 | } 409 | } 410 | 411 | impl<'a> FrameCtx<'a> { 412 | /// Produces a `CtxRef` ready for describing the UI for this frame. 413 | pub fn context(&self) -> CtxRef { 414 | self.ui.context.clone() 415 | } 416 | 417 | /// End the current frame, 418 | pub fn end(mut self) { 419 | self.end_inner(); 420 | } 421 | 422 | // The inner `end` implementation, shared between `end` and `drop`. 423 | fn end_inner(&mut self) { 424 | if !self.ended { 425 | self.ui.end_frame_inner(); 426 | self.ended = true; 427 | } 428 | } 429 | } 430 | 431 | impl<'a> Drop for FrameCtx<'a> { 432 | fn drop(&mut self) { 433 | self.end_inner(); 434 | } 435 | } 436 | 437 | impl<'a> Deref for FrameCtx<'a> { 438 | type Target = egui::CtxRef; 439 | fn deref(&self) -> &Self::Target { 440 | &self.ui.context 441 | } 442 | } 443 | 444 | impl epi::RepaintSignal for RepaintSignal { 445 | fn request_repaint(&self) { 446 | if let Ok(guard) = self.0.lock() { 447 | guard.wakeup().ok(); 448 | } 449 | } 450 | } 451 | 452 | /// Translates winit to egui keycodes. 453 | #[inline] 454 | fn winit_to_egui_key_code(key: VirtualKeyCode) -> Option { 455 | use egui::Key; 456 | 457 | Some(match key { 458 | VirtualKeyCode::Escape => Key::Escape, 459 | VirtualKeyCode::Insert => Key::Insert, 460 | VirtualKeyCode::Home => Key::Home, 461 | VirtualKeyCode::Delete => Key::Delete, 462 | VirtualKeyCode::End => Key::End, 463 | VirtualKeyCode::PageDown => Key::PageDown, 464 | VirtualKeyCode::PageUp => Key::PageUp, 465 | VirtualKeyCode::Left => Key::ArrowLeft, 466 | VirtualKeyCode::Up => Key::ArrowUp, 467 | VirtualKeyCode::Right => Key::ArrowRight, 468 | VirtualKeyCode::Down => Key::ArrowDown, 469 | VirtualKeyCode::Back => Key::Backspace, 470 | VirtualKeyCode::Return => Key::Enter, 471 | VirtualKeyCode::Tab => Key::Tab, 472 | VirtualKeyCode::Space => Key::Space, 473 | 474 | VirtualKeyCode::A => Key::A, 475 | VirtualKeyCode::B => Key::B, 476 | VirtualKeyCode::C => Key::C, 477 | VirtualKeyCode::D => Key::D, 478 | VirtualKeyCode::E => Key::E, 479 | VirtualKeyCode::F => Key::F, 480 | VirtualKeyCode::G => Key::G, 481 | VirtualKeyCode::H => Key::H, 482 | VirtualKeyCode::I => Key::I, 483 | VirtualKeyCode::J => Key::J, 484 | VirtualKeyCode::K => Key::K, 485 | VirtualKeyCode::L => Key::L, 486 | VirtualKeyCode::M => Key::M, 487 | VirtualKeyCode::N => Key::N, 488 | VirtualKeyCode::O => Key::O, 489 | VirtualKeyCode::P => Key::P, 490 | VirtualKeyCode::Q => Key::Q, 491 | VirtualKeyCode::R => Key::R, 492 | VirtualKeyCode::S => Key::S, 493 | VirtualKeyCode::T => Key::T, 494 | VirtualKeyCode::U => Key::U, 495 | VirtualKeyCode::V => Key::V, 496 | VirtualKeyCode::W => Key::W, 497 | VirtualKeyCode::X => Key::X, 498 | VirtualKeyCode::Y => Key::Y, 499 | VirtualKeyCode::Z => Key::Z, 500 | 501 | VirtualKeyCode::Key0 => Key::Num0, 502 | VirtualKeyCode::Key1 => Key::Num1, 503 | VirtualKeyCode::Key2 => Key::Num2, 504 | VirtualKeyCode::Key3 => Key::Num3, 505 | VirtualKeyCode::Key4 => Key::Num4, 506 | VirtualKeyCode::Key5 => Key::Num5, 507 | VirtualKeyCode::Key6 => Key::Num6, 508 | VirtualKeyCode::Key7 => Key::Num7, 509 | VirtualKeyCode::Key8 => Key::Num8, 510 | VirtualKeyCode::Key9 => Key::Num9, 511 | 512 | _ => { 513 | return None; 514 | } 515 | }) 516 | } 517 | 518 | /// Translates winit to egui modifier keys. 519 | #[inline] 520 | fn winit_to_egui_modifiers(modifiers: winit::event::ModifiersState) -> egui::Modifiers { 521 | egui::Modifiers { 522 | alt: modifiers.alt(), 523 | ctrl: modifiers.ctrl(), 524 | shift: modifiers.shift(), 525 | #[cfg(target_os = "macos")] 526 | mac_cmd: modifiers.logo(), 527 | #[cfg(target_os = "macos")] 528 | command: modifiers.logo(), 529 | #[cfg(not(target_os = "macos"))] 530 | mac_cmd: false, 531 | #[cfg(not(target_os = "macos"))] 532 | command: modifiers.ctrl(), 533 | } 534 | } 535 | 536 | pub fn edit_color(ui: &mut egui::Ui, color: &mut nannou::color::Hsv) { 537 | let mut egui_hsv = egui::color::Hsva::new( 538 | color.hue.to_positive_radians() as f32 / (std::f32::consts::PI * 2.0), 539 | color.saturation, 540 | color.value, 541 | 1.0, 542 | ); 543 | 544 | if egui::color_picker::color_edit_button_hsva( 545 | ui, 546 | &mut egui_hsv, 547 | egui::color_picker::Alpha::Opaque, 548 | ) 549 | .changed() 550 | { 551 | *color = nannou::color::hsv(egui_hsv.h, egui_hsv.s, egui_hsv.v); 552 | } 553 | } 554 | 555 | /// We only want printable characters and ignore all special keys. 556 | fn is_printable(chr: char) -> bool { 557 | let is_in_private_use_area = '\u{e000}' <= chr && chr <= '\u{f8ff}' 558 | || '\u{f0000}' <= chr && chr <= '\u{ffffd}' 559 | || '\u{100000}' <= chr && chr <= '\u{10fffd}'; 560 | !is_in_private_use_area && !chr.is_ascii_control() 561 | } 562 | -------------------------------------------------------------------------------- /nannou_egui_demo_app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nannou_egui_demo_app" 3 | version = "0.1.0" 4 | authors = ["mitchmindtree "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [dependencies] 9 | nannou_egui = { version = "0.5", path = "../nannou_egui" } 10 | nannou = "0.18.1" 11 | egui_demo_lib = { version = "0.15" } 12 | -------------------------------------------------------------------------------- /nannou_egui_demo_app/src/main.rs: -------------------------------------------------------------------------------- 1 | use nannou::prelude::*; 2 | use nannou_egui::{egui_wgpu_backend::epi::App as EguiApp, Egui}; 3 | 4 | fn main() { 5 | nannou::app(model).update(update).run(); 6 | } 7 | 8 | struct Model { 9 | egui: Egui, 10 | egui_demo_app: egui_demo_lib::WrapApp, 11 | } 12 | 13 | fn model(app: &App) -> Model { 14 | app.set_loop_mode(LoopMode::wait()); 15 | let w_id = app 16 | .new_window() 17 | .raw_event(raw_window_event) 18 | .view(view) 19 | .build() 20 | .unwrap(); 21 | let window = app.window(w_id).unwrap(); 22 | let mut egui = Egui::from_window(&window); 23 | let mut egui_demo_app = egui_demo_lib::WrapApp::default(); 24 | let proxy = app.create_proxy(); 25 | egui.do_frame_with_epi_frame(proxy, |ctx, epi_frame| { 26 | egui_demo_app.setup(&ctx, epi_frame, None); 27 | }); 28 | Model { 29 | egui, 30 | egui_demo_app, 31 | } 32 | } 33 | 34 | fn raw_window_event(_app: &App, model: &mut Model, event: &nannou::winit::event::WindowEvent) { 35 | model.egui.handle_raw_event(event); 36 | } 37 | 38 | fn update(app: &App, model: &mut Model, update: Update) { 39 | let Model { 40 | ref mut egui, 41 | ref mut egui_demo_app, 42 | .. 43 | } = *model; 44 | egui.set_elapsed_time(update.since_start); 45 | let proxy = app.create_proxy(); 46 | egui.do_frame_with_epi_frame(proxy, |ctx, frame| { 47 | egui_demo_app.update(&ctx, frame); 48 | }); 49 | } 50 | 51 | fn view(_app: &App, model: &Model, frame: Frame) { 52 | model.egui.draw_to_frame(&frame).unwrap(); 53 | } 54 | --------------------------------------------------------------------------------