From 0faf731b637b80c4f1f0c0afd8aa5ce936039262 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 22 Feb 2022 14:41:16 +0100 Subject: [PATCH] Introduce egui::FullOutput, returned from Context::run --- README.md | 13 ++++--- egui-winit/src/epi.rs | 55 ++++++++++++++-------------- egui-winit/src/lib.rs | 8 ++--- egui/src/context.rs | 57 ++++++++++++++--------------- egui/src/data/output.rs | 58 ++++++++++++++++++++++-------- egui/src/lib.rs | 13 ++++--- egui_demo_lib/benches/benchmark.rs | 8 ++--- egui_demo_lib/src/lib.rs | 8 ++--- egui_glium/src/epi_backend.rs | 11 ++++-- egui_glium/src/lib.rs | 17 +++++---- egui_glow/src/epi_backend.rs | 10 +++++- egui_glow/src/lib.rs | 14 +++++--- egui_web/src/backend.rs | 21 +++++------ 13 files changed, 170 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 99381df3d81..54c808d3b9f 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ Missing an integration for the thing you're working on? Create one, it's easy! ### Writing your own egui integration -You need to collect [`egui::RawInput`](https://docs.rs/egui/latest/egui/struct.RawInput.html), paint [`egui::ClippedMesh`](https://docs.rs/epaint/latest/epaint/struct.ClippedMesh.html):es and handle [`egui::Output`](https://docs.rs/egui/latest/egui/struct.Output.html). The basic structure is this: +You need to collect [`egui::RawInput`](https://docs.rs/egui/latest/egui/struct.RawInput.html) and handle [`egui::FullOutput`](https://docs.rs/egui/latest/egui/struct.FullOutput.html). The basic structure is this: ``` rust let mut egui_ctx = egui::CtxRef::default(); @@ -201,20 +201,19 @@ let mut egui_ctx = egui::CtxRef::default(); loop { // Gather input (mouse, touches, keyboard, screen size, etc): let raw_input: egui::RawInput = my_integration.gather_input(); - let (output, shapes) = egui_ctx.run(raw_input, |egui_ctx| { + let full_output = egui_ctx.run(raw_input, |egui_ctx| { my_app.ui(egui_ctx); // add panels, windows and widgets to `egui_ctx` here }); - let clipped_meshes = egui_ctx.tessellate(shapes); // creates triangles to paint + let clipped_meshes = egui_ctx.tessellate(full_output.shapes); // creates triangles to paint - my_integration.set_egui_textures(&output.textures_delta.set); - my_integration.paint(clipped_meshes); - my_integration.free_egui_textures(&output.textures_delta.free); + my_integration.paint(&full_output.textures_delta, clipped_meshes); + let output = full_output.output; my_integration.set_cursor_icon(output.cursor_icon); if !output.copied_text.is_empty() { my_integration.set_clipboard_text(output.copied_text); } - // See `egui::Output` for more + // See `egui::FullOutput` for more } ``` diff --git a/egui-winit/src/epi.rs b/egui-winit/src/epi.rs index 70d13d32cc9..55fda23d06f 100644 --- a/egui-winit/src/epi.rs +++ b/egui-winit/src/epi.rs @@ -1,7 +1,4 @@ -use egui::Vec2; -use winit::dpi::LogicalSize; - -pub fn points_to_size(points: Vec2) -> LogicalSize { +pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize { winit::dpi::LogicalSize { width: points.x as f64, height: points.y as f64, @@ -222,6 +219,7 @@ pub struct EpiIntegration { frame: epi::Frame, persistence: crate::epi::Persistence, pub egui_ctx: egui::Context, + pending_full_output: egui::FullOutput, egui_winit: crate::State, pub app: Box, /// When set, it is time to quit @@ -267,6 +265,7 @@ impl EpiIntegration { persistence, egui_ctx, egui_winit: crate::State::new(max_texture_side, window), + pending_full_output: Default::default(), app, quit: false, can_drag_window: false, @@ -295,8 +294,8 @@ impl EpiIntegration { fn warm_up(&mut self, window: &winit::window::Window) { let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); self.egui_ctx.memory().set_everything_is_visible(true); - let (_, textures_delta, _) = self.update(window); - self.egui_ctx.output().textures_delta = textures_delta; // Handle it next frame + let full_output = self.update(window); + self.pending_full_output.append(full_output); // Handle it next frame *self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge. self.egui_ctx.clear_animations(); } @@ -323,37 +322,39 @@ impl EpiIntegration { self.egui_winit.on_event(&self.egui_ctx, event); } - /// Returns `needs_repaint` and shapes to paint. - pub fn update( - &mut self, - window: &winit::window::Window, - ) -> (bool, egui::TexturesDelta, Vec) { + pub fn update(&mut self, window: &winit::window::Window) -> egui::FullOutput { let frame_start = instant::Instant::now(); let raw_input = self.egui_winit.take_egui_input(window); - let (egui_output, shapes) = self.egui_ctx.run(raw_input, |egui_ctx| { + let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { self.app.update(egui_ctx, &self.frame); }); - - let needs_repaint = egui_output.needs_repaint; - let textures_delta = self - .egui_winit - .handle_output(window, &self.egui_ctx, egui_output); - - let mut app_output = self.frame.take_app_output(); - app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108 - self.can_drag_window = false; - - if app_output.quit { - self.quit = self.app.on_exit_event(); + self.pending_full_output.append(full_output); + let full_output = std::mem::take(&mut self.pending_full_output); + + { + let mut app_output = self.frame.take_app_output(); + app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108 + self.can_drag_window = false; + if app_output.quit { + self.quit = self.app.on_exit_event(); + } + crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output); } - crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output); - let frame_time = (instant::Instant::now() - frame_start).as_secs_f64() as f32; self.frame.lock().info.cpu_usage = Some(frame_time); - (needs_repaint, textures_delta, shapes) + full_output + } + + pub fn handle_egui_output( + &mut self, + window: &winit::window::Window, + egui_output: egui::Output, + ) { + self.egui_winit + .handle_egui_output(window, &self.egui_ctx, egui_output); } pub fn maybe_autosave(&mut self, window: &winit::window::Window) { diff --git a/egui-winit/src/lib.rs b/egui-winit/src/lib.rs index d1a62c14f42..7072659eb83 100644 --- a/egui-winit/src/lib.rs +++ b/egui-winit/src/lib.rs @@ -514,12 +514,12 @@ impl State { /// * open any clicked urls /// * update the IME /// * - pub fn handle_output( + pub fn handle_egui_output( &mut self, window: &winit::window::Window, egui_ctx: &egui::Context, output: egui::Output, - ) -> egui::TexturesDelta { + ) { if egui_ctx.options().screen_reader { self.screen_reader.speak(&output.events_description()); } @@ -528,11 +528,9 @@ impl State { cursor_icon, open_url, copied_text, - needs_repaint: _, // needs to be handled elsewhere events: _, // handled above mutable_text_under_cursor: _, // only used in egui_web text_cursor_pos, - textures_delta, } = output; self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI @@ -550,8 +548,6 @@ impl State { if let Some(egui::Pos2 { x, y }) = text_cursor_pos { window.set_ime_position(winit::dpi::LogicalPosition { x, y }); } - - textures_delta } fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) { diff --git a/egui/src/context.rs b/egui/src/context.rs index f9d64d39fa2..051dfa08619 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -2,7 +2,7 @@ use crate::{ animation_manager::AnimationManager, data::output::Output, frame_state::FrameState, - input_state::*, layers::GraphicLayers, memory::Options, TextureHandle, *, + input_state::*, layers::GraphicLayers, memory::Options, output::FullOutput, TextureHandle, *, }; use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; @@ -122,13 +122,13 @@ impl ContextImpl { /// /// ``` no_run /// # fn handle_output(_: egui::Output) {} -/// # fn paint(_: Vec) {} +/// # fn paint(textures_detla: egui::TexturesDelta, _: Vec) {} /// let mut ctx = egui::Context::default(); /// /// // Game loop: /// loop { /// let raw_input = egui::RawInput::default(); -/// let (output, shapes) = ctx.run(raw_input, |ctx| { +/// let full_output = ctx.run(raw_input, |ctx| { /// egui::CentralPanel::default().show(&ctx, |ui| { /// ui.label("Hello world!"); /// if ui.button("Click me").clicked() { @@ -136,9 +136,9 @@ impl ContextImpl { /// } /// }); /// }); -/// let clipped_meshes = ctx.tessellate(shapes); // create triangles to paint -/// handle_output(output); -/// paint(clipped_meshes); +/// handle_output(full_output.output); +/// let clipped_meshes = ctx.tessellate(full_output.shapes); // create triangles to paint +/// paint(full_output.textures_delta, clipped_meshes); /// } /// ``` #[derive(Clone)] @@ -185,19 +185,15 @@ impl Context { /// /// // Each frame: /// let input = egui::RawInput::default(); - /// let (output, shapes) = ctx.run(input, |ctx| { + /// let full_output = ctx.run(input, |ctx| { /// egui::CentralPanel::default().show(&ctx, |ui| { /// ui.label("Hello egui!"); /// }); /// }); - /// // handle output, paint shapes + /// // handle full_output /// ``` #[must_use] - pub fn run( - &self, - new_input: RawInput, - run_ui: impl FnOnce(&Context), - ) -> (Output, Vec) { + pub fn run(&self, new_input: RawInput, run_ui: impl FnOnce(&Context)) -> FullOutput { self.begin_frame(new_input); run_ui(self); self.end_frame() @@ -217,8 +213,8 @@ impl Context { /// ui.label("Hello egui!"); /// }); /// - /// let (output, shapes) = ctx.end_frame(); - /// // handle output, paint shapes + /// let full_output = ctx.end_frame(); + /// // handle full_output /// ``` pub fn begin_frame(&self, new_input: RawInput) { self.write().begin_frame_mut(new_input); @@ -719,14 +715,13 @@ impl Context { impl Context { /// Call at the end of each frame. - /// Returns what has happened this frame [`crate::Output`] as well as what you need to paint. - /// You can transform the returned shapes into triangles with a call to [`Context::tessellate`]. #[must_use] - pub fn end_frame(&self) -> (Output, Vec) { + pub fn end_frame(&self) -> FullOutput { if self.input().wants_repaint() { self.request_repaint(); } + let textures_delta; { let ctx_impl = &mut *self.write(); ctx_impl @@ -742,20 +737,26 @@ impl Context { .set(TextureId::default(), font_image_delta); } - ctx_impl - .output - .textures_delta - .append(ctx_impl.tex_manager.0.write().take_delta()); - } + textures_delta = ctx_impl.tex_manager.0.write().take_delta(); + }; + + let output: Output = std::mem::take(&mut self.output()); - let mut output: Output = std::mem::take(&mut self.output()); - if self.read().repaint_requests > 0 { + let needs_repaint = if self.read().repaint_requests > 0 { self.write().repaint_requests -= 1; - output.needs_repaint = true; - } + true + } else { + false + }; let shapes = self.drain_paint_lists(); - (output, shapes) + + FullOutput { + output, + needs_repaint, + textures_delta, + shapes, + } } fn drain_paint_lists(&self) -> Vec { diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 664808b5f00..666b6189a0b 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -3,6 +3,49 @@ use crate::WidgetType; /// What egui emits each frame. +/// +/// The backend should use this. +#[derive(Clone, Default, PartialEq)] +pub struct FullOutput { + /// Non-rendering related output. + pub output: Output, + + /// If `true`, egui is requesting immediate repaint (i.e. on the next frame). + /// + /// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`. + pub needs_repaint: bool, + + /// Texture changes since last frame (including the font texture). + /// + /// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting, + /// and free any texture in [`crate::TexturesDelta::free`] _after_ painting. + pub textures_delta: epaint::textures::TexturesDelta, + + /// What to paint. + /// + /// You can use [`crate::Context::tessellate`] to turn this into triangles. + pub shapes: Vec, +} + +impl FullOutput { + /// Add on new output. + pub fn append(&mut self, newer: Self) { + let Self { + output, + needs_repaint, + textures_delta, + shapes, + } = newer; + + self.output.append(output); + self.needs_repaint = needs_repaint; // if the last frame doesn't need a repaint, then we don't need to repaint + self.textures_delta.append(textures_delta); + self.shapes = shapes; // Only paint the latest + } +} + +/// The non-rendering part of what egui emits each frame. +/// /// The backend should use this. #[derive(Clone, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -18,14 +61,6 @@ pub struct Output { /// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`]. pub copied_text: String, - /// If `true`, egui is requesting immediate repaint (i.e. on the next frame). - /// - /// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`. - /// - /// As an egui user: don't set this value directly. - /// Call `Context::request_repaint()` instead and it will do so for you. - pub needs_repaint: bool, - /// Events that may be useful to e.g. a screen reader. pub events: Vec, @@ -35,9 +70,6 @@ pub struct Output { /// Screen-space position of text edit cursor (used for IME). pub text_cursor_pos: Option, - - /// Texture changes since last frame. - pub textures_delta: epaint::textures::TexturesDelta, } impl Output { @@ -70,11 +102,9 @@ impl Output { cursor_icon, open_url, copied_text, - needs_repaint, mut events, mutable_text_under_cursor, text_cursor_pos, - textures_delta, } = newer; self.cursor_icon = cursor_icon; @@ -84,11 +114,9 @@ impl Output { if !copied_text.is_empty() { self.copied_text = copied_text; } - self.needs_repaint = needs_repaint; // if the last frame doesn't need a repaint, then we don't need to repaint self.events.append(&mut events); self.mutable_text_under_cursor = mutable_text_under_cursor; self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos); - self.textures_delta.append(textures_delta); } /// Take everything ephemeral (everything except `cursor_icon` currently) diff --git a/egui/src/lib.rs b/egui/src/lib.rs index ee472935fef..90a27c24536 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -111,15 +111,15 @@ //! //! ``` no_run //! # fn handle_output(_: egui::Output) {} -//! # fn paint(_: Vec) {} //! # fn gather_input() -> egui::RawInput { egui::RawInput::default() } +//! # fn paint(textures_detla: egui::TexturesDelta, _: Vec) {} //! let mut ctx = egui::Context::default(); //! //! // Game loop: //! loop { //! let raw_input: egui::RawInput = gather_input(); //! -//! let (output, shapes) = ctx.run(raw_input, |ctx| { +//! let full_output = ctx.run(raw_input, |ctx| { //! egui::CentralPanel::default().show(&ctx, |ui| { //! ui.label("Hello world!"); //! if ui.button("Click me").clicked() { @@ -127,10 +127,9 @@ //! } //! }); //! }); -//! -//! let clipped_meshes = ctx.tessellate(shapes); // create triangles to paint -//! handle_output(output); -//! paint(clipped_meshes); +//! handle_output(full_output.output); +//! let clipped_meshes = ctx.tessellate(full_output.shapes); // create triangles to paint +//! paint(full_output.textures_delta, clipped_meshes); //! } //! ``` //! @@ -403,7 +402,7 @@ pub use { context::Context, data::{ input::*, - output::{self, CursorIcon, Output, WidgetInfo}, + output::{self, CursorIcon, FullOutput, Output, WidgetInfo}, }, grid::Grid, id::{Id, IdMap}, diff --git a/egui_demo_lib/benches/benchmark.rs b/egui_demo_lib/benches/benchmark.rs index 584afb7589e..4fdeb7301fd 100644 --- a/egui_demo_lib/benches/benchmark.rs +++ b/egui_demo_lib/benches/benchmark.rs @@ -13,10 +13,10 @@ pub fn criterion_benchmark(c: &mut Criterion) { // The most end-to-end benchmark. c.bench_function("demo_with_tessellate__realistic", |b| { b.iter(|| { - let (_output, shapes) = ctx.run(RawInput::default(), |ctx| { + let full_output = ctx.run(RawInput::default(), |ctx| { demo_windows.ui(ctx); }); - ctx.tessellate(shapes) + ctx.tessellate(full_output.shapes) }) }); @@ -28,11 +28,11 @@ pub fn criterion_benchmark(c: &mut Criterion) { }) }); - let (_output, shapes) = ctx.run(RawInput::default(), |ctx| { + let full_output = ctx.run(RawInput::default(), |ctx| { demo_windows.ui(ctx); }); c.bench_function("demo_only_tessellate", |b| { - b.iter(|| ctx.tessellate(shapes.clone())) + b.iter(|| ctx.tessellate(full_output.shapes.clone())) }); } diff --git a/egui_demo_lib/src/lib.rs b/egui_demo_lib/src/lib.rs index f21f22d58ad..96112481669 100644 --- a/egui_demo_lib/src/lib.rs +++ b/egui_demo_lib/src/lib.rs @@ -145,10 +145,10 @@ fn test_egui_e2e() { const NUM_FRAMES: usize = 5; for _ in 0..NUM_FRAMES { - let (_output, shapes) = ctx.run(raw_input.clone(), |ctx| { + let full_output = ctx.run(raw_input.clone(), |ctx| { demo_windows.ui(ctx); }); - let clipped_meshes = ctx.tessellate(shapes); + let clipped_meshes = ctx.tessellate(full_output.shapes); assert!(!clipped_meshes.is_empty()); } } @@ -164,10 +164,10 @@ fn test_egui_zero_window_size() { const NUM_FRAMES: usize = 5; for _ in 0..NUM_FRAMES { - let (_output, shapes) = ctx.run(raw_input.clone(), |ctx| { + let full_output = ctx.run(raw_input.clone(), |ctx| { demo_windows.ui(ctx); }); - let clipped_meshes = ctx.tessellate(shapes); + let clipped_meshes = ctx.tessellate(full_output.shapes); assert!(clipped_meshes.is_empty(), "There should be nothing to show"); } } diff --git a/egui_glium/src/epi_backend.rs b/egui_glium/src/epi_backend.rs index 61dafe27dde..e0d44cbb779 100644 --- a/egui_glium/src/epi_backend.rs +++ b/egui_glium/src/epi_backend.rs @@ -67,8 +67,15 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { std::thread::sleep(std::time::Duration::from_millis(10)); } - let (needs_repaint, textures_delta, shapes) = - integration.update(display.gl_window().window()); + let egui::FullOutput { + output, + needs_repaint, + textures_delta, + shapes, + } = integration.update(display.gl_window().window()); + + integration.handle_egui_output(display.gl_window().window(), output); + let clipped_meshes = integration.egui_ctx.tessellate(shapes); // paint: diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 4bf8eb8e5a5..dcd9342ee53 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -141,16 +141,19 @@ impl EguiGlium { let raw_input = self .egui_winit .take_egui_input(display.gl_window().window()); - let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui); - let needs_repaint = egui_output.needs_repaint; - let textures_delta = self.egui_winit.handle_output( - display.gl_window().window(), - &self.egui_ctx, - egui_output, - ); + let egui::FullOutput { + output, + needs_repaint, + textures_delta, + shapes, + } = self.egui_ctx.run(raw_input, run_ui); + + self.egui_winit + .handle_egui_output(display.gl_window().window(), &self.egui_ctx, output); self.shapes = shapes; self.textures_delta.append(textures_delta); + needs_repaint } diff --git a/egui_glow/src/epi_backend.rs b/egui_glow/src/epi_backend.rs index 54045e4ef26..6c7d78a44c5 100644 --- a/egui_glow/src/epi_backend.rs +++ b/egui_glow/src/epi_backend.rs @@ -83,7 +83,15 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { std::thread::sleep(std::time::Duration::from_millis(10)); } - let (needs_repaint, textures_delta, shapes) = integration.update(gl_window.window()); + let egui::FullOutput { + output, + needs_repaint, + textures_delta, + shapes, + } = integration.update(gl_window.window()); + + integration.handle_egui_output(gl_window.window(), output); + let clipped_meshes = integration.egui_ctx.tessellate(shapes); // paint: diff --git a/egui_glow/src/lib.rs b/egui_glow/src/lib.rs index 84b912093a0..6e5c8f2726f 100644 --- a/egui_glow/src/lib.rs +++ b/egui_glow/src/lib.rs @@ -156,11 +156,15 @@ impl EguiGlow { run_ui: impl FnMut(&egui::Context), ) -> bool { let raw_input = self.egui_winit.take_egui_input(window); - let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui); - let needs_repaint = egui_output.needs_repaint; - let textures_delta = self - .egui_winit - .handle_output(window, &self.egui_ctx, egui_output); + let egui::FullOutput { + output, + needs_repaint, + textures_delta, + shapes, + } = self.egui_ctx.run(raw_input, run_ui); + + self.egui_winit + .handle_egui_output(window, &self.egui_ctx, output); self.shapes = shapes; self.textures_delta.append(textures_delta); diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 22461e48ae5..db413ac78c8 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -267,14 +267,19 @@ impl AppRunner { let canvas_size = canvas_size_in_points(self.canvas_id()); let raw_input = self.input.new_frame(canvas_size); - let (egui_output, shapes) = self.egui_ctx.run(raw_input, |egui_ctx| { + let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { self.app.update(egui_ctx, &self.frame); }); - let clipped_meshes = self.egui_ctx.tessellate(shapes); + let egui::FullOutput { + output, + needs_repaint, + textures_delta, + shapes, + } = full_output; - let needs_repaint = egui_output.needs_repaint; - let textures_delta = self.handle_egui_output(egui_output); + self.handle_egui_output(output); self.textures_delta.append(textures_delta); + let clipped_meshes = self.egui_ctx.tessellate(shapes); { let app_output = self.frame.take_app_output(); @@ -306,7 +311,7 @@ impl AppRunner { Ok(()) } - fn handle_egui_output(&mut self, output: egui::Output) -> egui::TexturesDelta { + fn handle_egui_output(&mut self, output: egui::Output) { if self.egui_ctx.options().screen_reader { self.screen_reader.speak(&output.events_description()); } @@ -315,11 +320,9 @@ impl AppRunner { cursor_icon, open_url, copied_text, - needs_repaint: _, // handled elsewhere - events: _, // already handled + events: _, // already handled mutable_text_under_cursor, text_cursor_pos, - textures_delta, } = output; set_cursor_icon(cursor_icon); @@ -341,8 +344,6 @@ impl AppRunner { text_agent::move_text_cursor(text_cursor_pos, self.canvas_id()); self.text_cursor_pos = text_cursor_pos; } - - textures_delta } }