diff --git a/CHANGELOG.md b/CHANGELOG.md index 76400471bd6..9120a867944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,21 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * Added `Ui::add_visible` and `Ui::add_visible_ui`. ### Changed 🔧 +* ⚠️ `Context::input` and `Ui::input` now locks a mutex. This can lead to a dead-lock is used in an `if let` binding! + * `if let Some(pos) = ui.input().pointer.latest_pos()` and similar must now be rewritten on two lines, or with added `{}` around the righ-hand-side. + * Search for this problem in your code using the regex `if let .*input`. +* Renamed `CtxRef` to `Context` ([#1050](https://github.com/emilk/egui/pull/1050)). +* `Context` can now be cloned and stored between frames ([#1050](https://github.com/emilk/egui/pull/1050)). * Renamed `Ui::visible` to `Ui::is_visible`. * Split `Event::Text` into `Event::Text` and `Event::Paste` ([#1058](https://github.com/emilk/egui/pull/1058)). ### Fixed 🐛 * Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)) +### Contributors 🙏 +* [danielkeller](https://github.com/danielkeller): [#1050](https://github.com/emilk/egui/pull/1050). + + ## 0.16.1 - 2021-12-31 - Add back `CtxRef::begin_frame,end_frame` ### Added ⭐ diff --git a/README.md b/README.md index ab50bdd02e5..5932266943e 100644 --- a/README.md +++ b/README.md @@ -365,6 +365,7 @@ Notable contributions by: * [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685). * [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543). * [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868). +* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050). * And [many more](https://github.com/emilk/egui/graphs/contributors?type=a). egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE). diff --git a/eframe/examples/custom_font.rs b/eframe/examples/custom_font.rs index 199521dab3d..28af4c06b6a 100644 --- a/eframe/examples/custom_font.rs +++ b/eframe/examples/custom_font.rs @@ -19,7 +19,7 @@ impl epi::App for MyApp { fn setup( &mut self, - ctx: &egui::CtxRef, + ctx: &egui::Context, _frame: &epi::Frame, _storage: Option<&dyn epi::Storage>, ) { @@ -51,7 +51,7 @@ impl epi::App for MyApp { ctx.set_fonts(fonts); } - fn update(&mut self, ctx: &egui::CtxRef, _frame: &epi::Frame) { + fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) { egui::CentralPanel::default().show(ctx, |ui| { ui.heading("egui using custom fonts"); ui.text_edit_multiline(&mut self.text); diff --git a/eframe/examples/file_dialog.rs b/eframe/examples/file_dialog.rs index b9767e3e0b6..b7e17e47c91 100644 --- a/eframe/examples/file_dialog.rs +++ b/eframe/examples/file_dialog.rs @@ -13,7 +13,7 @@ impl epi::App for MyApp { "Native file dialogs and drag-and-drop files" } - fn update(&mut self, ctx: &egui::CtxRef, _frame: &epi::Frame) { + fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) { egui::CentralPanel::default().show(ctx, |ui| { ui.label("Drag-and-drop files onto the window!"); @@ -57,7 +57,7 @@ impl epi::App for MyApp { } impl MyApp { - fn detect_files_being_dropped(&mut self, ctx: &egui::CtxRef) { + fn detect_files_being_dropped(&mut self, ctx: &egui::Context) { use egui::*; // Preview hovering files: diff --git a/eframe/examples/hello_world.rs b/eframe/examples/hello_world.rs index 9a94492975e..7ec61fba7ae 100644 --- a/eframe/examples/hello_world.rs +++ b/eframe/examples/hello_world.rs @@ -21,7 +21,7 @@ impl epi::App for MyApp { "My egui App" } - fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) { + fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { let Self { name, age } = self; egui::CentralPanel::default().show(ctx, |ui| { diff --git a/eframe/examples/image.rs b/eframe/examples/image.rs index 91e0f4f5142..9a26ffce217 100644 --- a/eframe/examples/image.rs +++ b/eframe/examples/image.rs @@ -12,7 +12,7 @@ impl epi::App for MyApp { "Show an image with eframe/egui" } - fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) { + fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { if self.texture.is_none() { // Load the image: let image_data = include_bytes!("rust-logo-256x256.png"); diff --git a/eframe/src/lib.rs b/eframe/src/lib.rs index 2fff75192fb..d0ba574db99 100644 --- a/eframe/src/lib.rs +++ b/eframe/src/lib.rs @@ -26,7 +26,7 @@ //! "My egui App" //! } //! -//! fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) { +//! fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { //! egui::CentralPanel::default().show(ctx, |ui| { //! ui.heading("Hello World!"); //! }); @@ -129,7 +129,7 @@ pub fn start_web(canvas_id: &str, app: Box) -> Result<(), wasm_bin /// "My egui App" /// } /// -/// fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) { +/// fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { /// egui::CentralPanel::default().show(ctx, |ui| { /// ui.heading("Hello World!"); /// }); @@ -160,7 +160,7 @@ pub fn run_native(app: Box, native_options: epi::NativeOptions) -> /// "My egui App" /// } /// -/// fn update(&mut self, ctx: &egui::CtxRef, frame: &epi::Frame) { +/// fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { /// egui::CentralPanel::default().show(ctx, |ui| { /// ui.heading("Hello World!"); /// }); diff --git a/egui-winit/src/epi.rs b/egui-winit/src/epi.rs index badea97aaeb..ea4353bf92b 100644 --- a/egui-winit/src/epi.rs +++ b/egui-winit/src/epi.rs @@ -191,7 +191,7 @@ impl Persistence { pub struct EpiIntegration { frame: epi::Frame, persistence: crate::epi::Persistence, - pub egui_ctx: egui::CtxRef, + pub egui_ctx: egui::Context, egui_winit: crate::State, pub app: Box, /// When set, it is time to quit @@ -206,7 +206,7 @@ impl EpiIntegration { persistence: crate::epi::Persistence, app: Box, ) -> Self { - let egui_ctx = egui::CtxRef::default(); + let egui_ctx = egui::Context::default(); *egui_ctx.memory() = persistence.load_memory().unwrap_or_default(); @@ -250,7 +250,7 @@ impl EpiIntegration { } fn warm_up(&mut self, window: &winit::window::Window) { - let saved_memory = self.egui_ctx.memory().clone(); + let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); self.egui_ctx.memory().set_everything_is_visible(true); let (_, tex_alloc_data, _) = self.update(window); self.frame.lock().output.tex_allocation_data = tex_alloc_data; // handle it next frame diff --git a/egui/src/containers/area.rs b/egui/src/containers/area.rs index cb5fc831a86..9ff25d5fbbd 100644 --- a/egui/src/containers/area.rs +++ b/egui/src/containers/area.rs @@ -176,7 +176,7 @@ pub(crate) struct Prepared { impl Area { pub fn show( self, - ctx: &CtxRef, + ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { let prepared = self.begin(ctx); @@ -186,7 +186,7 @@ impl Area { InnerResponse { inner, response } } - pub(crate) fn begin(self, ctx: &CtxRef) -> Prepared { + pub(crate) fn begin(self, ctx: &Context) -> Prepared { let Area { id, movable, @@ -234,7 +234,7 @@ impl Area { } } - pub fn show_open_close_animation(&self, ctx: &CtxRef, frame: &Frame, is_open: bool) { + pub fn show_open_close_animation(&self, ctx: &Context, frame: &Frame, is_open: bool) { // must be called first so animation managers know the latest state let visibility_factor = ctx.animate_bool(self.id.with("close_animation"), is_open); @@ -276,7 +276,7 @@ impl Prepared { self.drag_bounds } - pub(crate) fn content_ui(&self, ctx: &CtxRef) -> Ui { + pub(crate) fn content_ui(&self, ctx: &Context) -> Ui { let screen_rect = ctx.input().screen_rect(); let bounds = if let Some(bounds) = self.drag_bounds { @@ -317,7 +317,7 @@ impl Prepared { } #[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`. - pub(crate) fn end(self, ctx: &CtxRef, content_ui: Ui) -> Response { + pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response { let Prepared { layer_id, mut state, @@ -370,8 +370,9 @@ impl Prepared { } fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { - if let Some(pointer_pos) = ctx.input().pointer.interact_pos() { - ctx.input().pointer.any_pressed() && ctx.layer_id_at(pointer_pos) == Some(layer_id) + if let Some(pointer_pos) = ctx.pointer_interact_pos() { + let any_pressed = ctx.input().pointer.any_pressed(); + any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id) } else { false } diff --git a/egui/src/containers/panel.rs b/egui/src/containers/panel.rs index 8bdea2a89ae..487049d8989 100644 --- a/egui/src/containers/panel.rs +++ b/egui/src/containers/panel.rs @@ -199,7 +199,7 @@ impl SidePanel { let mut is_resizing = false; if resizable { let resize_id = id.with("__resize"); - if let Some(pointer) = ui.input().pointer.latest_pos() { + if let Some(pointer) = ui.ctx().latest_pointer_pos() { let we_are_on_top = ui .ctx() .layer_id_at(pointer) @@ -284,7 +284,7 @@ impl SidePanel { /// Show the panel at the top level. pub fn show( self, - ctx: &CtxRef, + ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { self.show_dyn(ctx, Box::new(add_contents)) @@ -293,7 +293,7 @@ impl SidePanel { /// Show the panel at the top level. fn show_dyn<'c, R>( self, - ctx: &CtxRef, + ctx: &Context, add_contents: Box R + 'c>, ) -> InnerResponse { let layer_id = LayerId::background(); @@ -485,7 +485,8 @@ impl TopBottomPanel { let mut is_resizing = false; if resizable { let resize_id = id.with("__resize"); - if let Some(pointer) = ui.input().pointer.latest_pos() { + let latest_pos = ui.input().pointer.latest_pos(); + if let Some(pointer) = latest_pos { let we_are_on_top = ui .ctx() .layer_id_at(pointer) @@ -570,7 +571,7 @@ impl TopBottomPanel { /// Show the panel at the top level. pub fn show( self, - ctx: &CtxRef, + ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { self.show_dyn(ctx, Box::new(add_contents)) @@ -579,7 +580,7 @@ impl TopBottomPanel { /// Show the panel at the top level. fn show_dyn<'c, R>( self, - ctx: &CtxRef, + ctx: &Context, add_contents: Box R + 'c>, ) -> InnerResponse { let layer_id = LayerId::background(); @@ -670,7 +671,7 @@ impl CentralPanel { /// Show the panel at the top level. pub fn show( self, - ctx: &CtxRef, + ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse { self.show_dyn(ctx, Box::new(add_contents)) @@ -679,7 +680,7 @@ impl CentralPanel { /// Show the panel at the top level. fn show_dyn<'c, R>( self, - ctx: &CtxRef, + ctx: &Context, add_contents: Box R + 'c>, ) -> InnerResponse { let available_rect = ctx.available_rect(); diff --git a/egui/src/containers/popup.rs b/egui/src/containers/popup.rs index 1194f12b869..76627ba6438 100644 --- a/egui/src/containers/popup.rs +++ b/egui/src/containers/popup.rs @@ -66,7 +66,11 @@ impl MonoState { /// } /// # }); /// ``` -pub fn show_tooltip(ctx: &CtxRef, id: Id, add_contents: impl FnOnce(&mut Ui) -> R) -> Option { +pub fn show_tooltip( + ctx: &Context, + id: Id, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { show_tooltip_at_pointer(ctx, id, add_contents) } @@ -88,7 +92,7 @@ pub fn show_tooltip(ctx: &CtxRef, id: Id, add_contents: impl FnOnce(&mut Ui) /// # }); /// ``` pub fn show_tooltip_at_pointer( - ctx: &CtxRef, + ctx: &Context, id: Id, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { @@ -104,7 +108,7 @@ pub fn show_tooltip_at_pointer( /// /// If the tooltip does not fit under the area, it tries to place it above it instead. pub fn show_tooltip_for( - ctx: &CtxRef, + ctx: &Context, id: Id, rect: &Rect, add_contents: impl FnOnce(&mut Ui) -> R, @@ -129,7 +133,7 @@ pub fn show_tooltip_for( /// /// Returns `None` if the tooltip could not be placed. pub fn show_tooltip_at( - ctx: &CtxRef, + ctx: &Context, id: Id, suggested_position: Option, add_contents: impl FnOnce(&mut Ui) -> R, @@ -146,7 +150,7 @@ pub fn show_tooltip_at( } fn show_tooltip_at_avoid_dyn<'c, R>( - ctx: &CtxRef, + ctx: &Context, mut id: Id, suggested_position: Option, above: bool, @@ -229,7 +233,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( /// } /// # }); /// ``` -pub fn show_tooltip_text(ctx: &CtxRef, id: Id, text: impl Into) -> Option<()> { +pub fn show_tooltip_text(ctx: &Context, id: Id, text: impl Into) -> Option<()> { show_tooltip(ctx, id, |ui| { crate::widgets::Label::new(text).ui(ui); }) @@ -237,7 +241,7 @@ pub fn show_tooltip_text(ctx: &CtxRef, id: Id, text: impl Into) -> O /// Show a pop-over window. fn show_tooltip_area_dyn<'c, R>( - ctx: &CtxRef, + ctx: &Context, id: Id, window_pos: Pos2, add_contents: Box R + 'c>, diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index a79bf5dfc5e..cf9f5e0475f 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -523,12 +523,11 @@ impl Prepared { }; let content_response = ui.interact(inner_rect, id.with("area"), sense); - let input = ui.input(); if content_response.dragged() { for d in 0..2 { if has_bar[d] { - state.offset[d] -= input.pointer.delta()[d]; - state.vel[d] = input.pointer.velocity()[d]; + state.offset[d] -= ui.input().pointer.delta()[d]; + state.vel[d] = ui.input().pointer.velocity()[d]; state.scroll_stuck_to_end[d] = false; } else { state.vel[d] = 0.0; @@ -537,7 +536,7 @@ impl Prepared { } else { let stop_speed = 20.0; // Pixels per second. let friction_coeff = 1000.0; // Pixels per second squared. - let dt = input.unstable_dt; + let dt = ui.input().unstable_dt; let friction = friction_coeff * dt; if friction > state.vel.length() || state.vel.length() < stop_speed { diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 4eceac70dfc..66afa97e286 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -235,7 +235,7 @@ impl<'open> Window<'open> { #[inline] pub fn show( self, - ctx: &CtxRef, + ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option>> { self.show_dyn(ctx, Box::new(add_contents)) @@ -243,7 +243,7 @@ impl<'open> Window<'open> { fn show_dyn<'c, R>( self, - ctx: &CtxRef, + ctx: &Context, add_contents: Box R + 'c>, ) -> Option>> { let Window { @@ -296,7 +296,7 @@ impl<'open> Window<'open> { .and_then(|window_interaction| { // Calculate roughly how much larger the window size is compared to the inner rect let title_bar_height = if with_title_bar { - title.font_height(ctx.fonts(), &ctx.style()) + title_content_spacing + title.font_height(ctx) + title_content_spacing } else { 0.0 }; @@ -757,7 +757,7 @@ fn show_title_bar( ) -> TitleBar { let inner_response = ui.horizontal(|ui| { let height = title - .font_height(ui.fonts(), ui.style()) + .font_height(ui.ctx()) .max(ui.spacing().interact_size.y); ui.set_min_height(height); diff --git a/egui/src/context.rs b/egui/src/context.rs index ad5c84829a2..ef9bd9762cf 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -1,40 +1,107 @@ // #![warn(missing_docs)] -use std::sync::{ - atomic::{AtomicU32, Ordering::SeqCst}, - Arc, -}; - use crate::{ - animation_manager::AnimationManager, - data::output::Output, - frame_state::FrameState, - input_state::*, - layers::GraphicLayers, - menu::ContextMenuSystem, - mutex::{Mutex, MutexGuard}, - *, + animation_manager::AnimationManager, data::output::Output, frame_state::FrameState, + input_state::*, layers::GraphicLayers, *, }; -use epaint::{stats::*, text::Fonts, *}; +use epaint::{mutex::*, stats::*, text::Fonts, *}; // ---------------------------------------------------------------------------- -/// A wrapper around [`Arc`](std::sync::Arc)`<`[`Context`]`>`. -/// This is how you will normally create and access a [`Context`]. +#[derive(Default)] +struct ContextImpl { + /// `None` until the start of the first frame. + fonts: Option, + memory: Memory, + animation_manager: AnimationManager, + + input: InputState, + + /// State that is collected during a frame and then cleared + frame_state: FrameState, + + // The output of a frame: + graphics: GraphicLayers, + output: Output, + + paint_stats: PaintStats, + + /// While positive, keep requesting repaints. Decrement at the end of each frame. + repaint_requests: u32, +} + +impl ContextImpl { + fn begin_frame_mut(&mut self, new_raw_input: RawInput) { + self.memory.begin_frame(&self.input, &new_raw_input); + + let mut input = std::mem::take(&mut self.input); + if let Some(new_pixels_per_point) = self.memory.new_pixels_per_point.take() { + input.pixels_per_point = new_pixels_per_point; + } + + self.input = input.begin_frame(new_raw_input); + self.frame_state.begin_frame(&self.input); + + self.update_fonts_mut(self.input.pixels_per_point()); + + // Ensure we register the background area so panels and background ui can catch clicks: + let screen_rect = self.input.screen_rect(); + self.memory.areas.set_state( + LayerId::background(), + containers::area::State { + pos: screen_rect.min, + size: screen_rect.size(), + interactable: true, + }, + ); + } + + /// Load fonts unless already loaded. + fn update_fonts_mut(&mut self, pixels_per_point: f32) { + let new_font_definitions = self.memory.new_font_definitions.take(); + + let pixels_per_point_changed = match &self.fonts { + None => true, + Some(current_fonts) => { + (current_fonts.pixels_per_point() - pixels_per_point).abs() > 1e-3 + } + }; + + if self.fonts.is_none() || new_font_definitions.is_some() || pixels_per_point_changed { + self.fonts = Some(Fonts::new( + pixels_per_point, + new_font_definitions.unwrap_or_else(|| { + self.fonts + .as_ref() + .map(|font| font.definitions().clone()) + .unwrap_or_default() + }), + )); + } + } +} + +// ---------------------------------------------------------------------------- + +/// Your handle to egui. +/// +/// This is the first thing you need when working with egui. +/// Contains the [`InputState`], [`Memory`], [`Output`], and more. +/// +/// [`Context`] is cheap to clone, and any clones refers to the same mutable data +/// ([`Context`] uses refcounting internally). /// -/// Almost all methods are marked `&self`, `Context` has interior mutability (protected by mutexes). +/// All methods are marked `&self`; `Context` has interior mutability (protected by a mutex). /// -/// [`CtxRef`] is cheap to clone, and any clones refers to the same mutable data. /// -/// A [`CtxRef`] is only valid for the duration of a frame, and so you should not store a [`CtxRef`] between frames. -/// A new [`CtxRef`] is created each frame by calling [`Self::run`]. +/// You can store /// /// # Example: /// /// ``` no_run /// # fn handle_output(_: egui::Output) {} /// # fn paint(_: Vec) {} -/// let mut ctx = egui::CtxRef::default(); +/// let mut ctx = egui::Context::default(); /// /// // Game loop: /// loop { @@ -53,58 +120,46 @@ use epaint::{stats::*, text::Fonts, *}; /// } /// ``` #[derive(Clone)] -pub struct CtxRef(std::sync::Arc); - -impl std::ops::Deref for CtxRef { - type Target = Context; +pub struct Context(Arc>); - fn deref(&self) -> &Context { - &*self.0 - } -} - -impl AsRef for CtxRef { - fn as_ref(&self) -> &Context { - self.0.as_ref() - } -} - -impl std::borrow::Borrow for CtxRef { - fn borrow(&self) -> &Context { - self.0.borrow() - } -} - -impl std::cmp::PartialEq for CtxRef { - fn eq(&self, other: &CtxRef) -> bool { +impl std::cmp::PartialEq for Context { + fn eq(&self, other: &Context) -> bool { Arc::ptr_eq(&self.0, &other.0) } } -impl Default for CtxRef { +impl Default for Context { fn default() -> Self { - Self(Arc::new(Context { + Self(Arc::new(RwLock::new(ContextImpl { // Start with painting an extra frame to compensate for some widgets // that take two frames before they "settle": - repaint_requests: AtomicU32::new(1), - ..Context::default() - })) + repaint_requests: 1, + ..ContextImpl::default() + }))) } } -impl CtxRef { +impl Context { + fn read(&self) -> RwLockReadGuard<'_, ContextImpl> { + self.0.read() + } + + fn write(&self) -> RwLockWriteGuard<'_, ContextImpl> { + self.0.write() + } + /// Run the ui code for one frame. /// /// Put your widgets into a [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`]. /// /// This will modify the internal reference to point to a new generation of [`Context`]. - /// Any old clones of this [`CtxRef`] will refer to the old [`Context`], which will not get new input. + /// Any old clones of this [`Context`] will refer to the old [`Context`], which will not get new input. /// /// You can alternatively run [`Self::begin_frame`] and [`Context::end_frame`]. /// /// ``` rust /// // One egui context that you keep reusing: - /// let mut ctx = egui::CtxRef::default(); + /// let mut ctx = egui::Context::default(); /// /// // Each frame: /// let input = egui::RawInput::default(); @@ -117,9 +172,9 @@ impl CtxRef { /// ``` #[must_use] pub fn run( - &mut self, + &self, new_input: RawInput, - run_ui: impl FnOnce(&CtxRef), + run_ui: impl FnOnce(&Context), ) -> (Output, Vec) { self.begin_frame(new_input); run_ui(self); @@ -130,7 +185,7 @@ impl CtxRef { /// /// ``` rust /// // One egui context that you keep reusing: - /// let mut ctx = egui::CtxRef::default(); + /// let mut ctx = egui::Context::default(); /// /// // Each frame: /// let input = egui::RawInput::default(); @@ -143,10 +198,8 @@ impl CtxRef { /// let (output, shapes) = ctx.end_frame(); /// // handle output, paint shapes /// ``` - pub fn begin_frame(&mut self, new_input: RawInput) { - let mut self_: Context = (*self.0).clone(); - self_.begin_frame_mut(new_input); - *self = Self(Arc::new(self_)); + pub fn begin_frame(&self, new_input: RawInput) { + self.write().begin_frame_mut(new_input); } // --------------------------------------------------------------------- @@ -167,7 +220,7 @@ impl CtxRef { let show_error = |pos: Pos2, text: String| { let painter = self.debug_painter(); let rect = painter.error(pos, text); - if let Some(pointer_pos) = self.input.pointer.hover_pos() { + if let Some(pointer_pos) = self.pointer_hover_pos() { if rect.contains(pointer_pos) { painter.error( rect.left_bottom() + vec2(2.0, 4.0), @@ -244,14 +297,19 @@ impl CtxRef { changed: false, // must be set by the widget itself }; - let mut memory = self.memory(); - if !enabled || !sense.focusable || !layer_id.allow_interaction() { // Not interested or allowed input: - memory.surrender_focus(id); + self.memory().surrender_focus(id); return response; } + self.register_interaction_id(id, rect); + + let clicked_elsewhere = response.clicked_elsewhere(); + let ctx_impl = &mut *self.write(); + let memory = &mut ctx_impl.memory; + let input = &mut ctx_impl.input; + // We only want to focus labels if the screen reader is on. let interested_in_focus = sense.interactive() || sense.focusable && memory.options.screen_reader; @@ -262,14 +320,12 @@ impl CtxRef { if sense.click && memory.has_focus(response.id) - && (self.input().key_pressed(Key::Space) || self.input().key_pressed(Key::Enter)) + && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) { // Space/enter works like a primary click for e.g. selected buttons response.clicked[PointerButton::Primary as usize] = true; } - self.register_interaction_id(id, rect); - if sense.click || sense.drag { memory.interaction.click_interest |= hovered && sense.click; memory.interaction.drag_interest |= hovered && sense.drag; @@ -278,7 +334,7 @@ impl CtxRef { response.is_pointer_button_down_on = memory.interaction.click_id == Some(id) || response.dragged; - for pointer_event in &self.input.pointer.pointer_events { + for pointer_event in &input.pointer.pointer_events { match pointer_event { PointerEvent::Moved(_) => {} PointerEvent::Pressed(_) => { @@ -325,14 +381,14 @@ impl CtxRef { } if response.is_pointer_button_down_on { - response.interact_pointer_pos = self.input().pointer.interact_pos(); + response.interact_pointer_pos = input.pointer.interact_pos(); } - if self.input.pointer.any_down() { + if input.pointer.any_down() { response.hovered &= response.is_pointer_button_down_on; // we don't hover widgets while interacting with *other* widgets } - if memory.has_focus(response.id) && response.clicked_elsewhere() { + if memory.has_focus(response.id) && clicked_elsewhere { memory.surrender_focus(id); } @@ -346,131 +402,99 @@ impl CtxRef { /// Get a full-screen painter for a new or existing layer pub fn layer_painter(&self, layer_id: LayerId) -> Painter { - Painter::new(self.clone(), layer_id, self.input.screen_rect()) + let screen_rect = self.input().screen_rect(); + Painter::new(self.clone(), layer_id, screen_rect) } /// Paint on top of everything else pub fn debug_painter(&self) -> Painter { Self::layer_painter(self, LayerId::debug()) } -} - -// ---------------------------------------------------------------------------- - -/// Your handle to egui. -/// -/// This is the first thing you need when working with egui. -/// Use [`CtxRef`] to create and refer to a [`Context`]. -/// -/// Contains the [`InputState`], [`Memory`], [`Output`], and more. -/// -/// Almost all methods are marked `&self`, [`Context`] has interior mutability (protected by mutexes). -/// Multi-threaded access to a [`Context`] is behind the feature flag `multi_threaded`. -/// Normally you'd always do all ui work on one thread, or perhaps use multiple contexts, -/// but if you really want to access the same [`Context`] from multiple threads, it *SHOULD* be fine, -/// but you are likely the first person to try it. -#[derive(Default)] -pub struct Context { - // We clone the Context each frame so we can set a new `input`. - // This is so we can avoid a mutex lock to access the `InputState`. - // This means everything else needs to be behind an Arc. - // We can probably come up with a nicer design. - // - /// `None` until the start of the first frame. - fonts: Option>, - memory: Arc>, - animation_manager: Arc>, - context_menu_system: Arc>, - - input: InputState, - - /// State that is collected during a frame and then cleared - frame_state: Arc>, - - // The output of a frame: - graphics: Arc>, - output: Arc>, - - paint_stats: Arc>, - - /// While positive, keep requesting repaints. Decrement at the end of each frame. - repaint_requests: AtomicU32, -} - -impl Clone for Context { - fn clone(&self) -> Self { - Context { - fonts: self.fonts.clone(), - memory: self.memory.clone(), - animation_manager: self.animation_manager.clone(), - input: self.input.clone(), - frame_state: self.frame_state.clone(), - graphics: self.graphics.clone(), - output: self.output.clone(), - paint_stats: self.paint_stats.clone(), - repaint_requests: self.repaint_requests.load(SeqCst).into(), - context_menu_system: self.context_menu_system.clone(), - } - } -} -impl Context { /// How much space is still available after panels has been added. /// This is the "background" area, what egui doesn't cover with panels (but may cover with windows). /// This is also the area to which windows are constrained. pub fn available_rect(&self) -> Rect { - self.frame_state.lock().available_rect() + self.frame_state().available_rect() } +} +/// ## Borrows parts of [`Context`] +impl Context { /// Stores all the egui state. /// If you want to store/restore egui, serialize this. - pub fn memory(&self) -> MutexGuard<'_, Memory> { - self.memory.lock() + pub fn memory(&self) -> RwLockWriteGuard<'_, Memory> { + RwLockWriteGuard::map(self.write(), |c| &mut c.memory) } - pub(crate) fn context_menu_system(&self) -> MutexGuard<'_, ContextMenuSystem> { - self.context_menu_system.lock() + pub(crate) fn graphics(&self) -> RwLockWriteGuard<'_, GraphicLayers> { + RwLockWriteGuard::map(self.write(), |c| &mut c.graphics) } - pub(crate) fn graphics(&self) -> MutexGuard<'_, GraphicLayers> { - self.graphics.lock() + /// What egui outputs each frame. + pub fn output(&self) -> RwLockWriteGuard<'_, Output> { + RwLockWriteGuard::map(self.write(), |c| &mut c.output) } - /// What egui outputs each frame. - pub fn output(&self) -> MutexGuard<'_, Output> { - self.output.lock() + pub(crate) fn frame_state(&self) -> RwLockWriteGuard<'_, FrameState> { + RwLockWriteGuard::map(self.write(), |c| &mut c.frame_state) + } + + /// Access the [`InputState`]. + /// + /// Note that this locks the [`Context`], so be careful with if-let bindings: + /// + /// ``` + /// # let mut ctx = egui::Context::default(); + /// if let Some(pos) = ctx.input().pointer.hover_pos() { + /// // ⚠️ Using `ctx` again here will lead to a dead-lock! + /// } + /// + /// if let Some(pos) = { ctx.input().pointer.hover_pos() } { + /// // This is fine! + /// } + /// + /// let pos = ctx.input().pointer.hover_pos(); + /// if let Some(pos) = pos { + /// // This is fine! + /// } + /// ``` + #[inline(always)] + pub fn input(&self) -> RwLockReadGuard<'_, InputState> { + RwLockReadGuard::map(self.read(), |c| &c.input) + } + + pub fn input_mut(&self) -> RwLockWriteGuard<'_, InputState> { + RwLockWriteGuard::map(self.write(), |c| &mut c.input) } - pub(crate) fn frame_state(&self) -> MutexGuard<'_, FrameState> { - self.frame_state.lock() + /// Not valid until first call to [`Context::run()`]. + /// That's because since we don't know the proper `pixels_per_point` until then. + pub fn fonts(&self) -> RwLockReadGuard<'_, Fonts> { + RwLockReadGuard::map(self.read(), |c| { + c.fonts + .as_ref() + .expect("No fonts available until first call to Context::run()") + }) } + fn fonts_mut(&self) -> RwLockWriteGuard<'_, Option> { + RwLockWriteGuard::map(self.write(), |c| &mut c.fonts) + } +} + +impl Context { /// Call this if there is need to repaint the UI, i.e. if you are showing an animation. /// If this is called at least once in a frame, then there will be another frame right after this. /// Call as many times as you wish, only one repaint will be issued. pub fn request_repaint(&self) { // request two frames of repaint, just to cover some corner cases (frame delays): - let times_to_repaint = 2; - self.repaint_requests.store(times_to_repaint, SeqCst); - } - - #[inline(always)] - pub fn input(&self) -> &InputState { - &self.input - } - - /// Not valid until first call to [`CtxRef::run()`]. - /// That's because since we don't know the proper `pixels_per_point` until then. - pub fn fonts(&self) -> &Fonts { - &*self - .fonts - .as_ref() - .expect("No fonts available until first call to CtxRef::run()") + self.write().repaint_requests = 2; } /// The egui font image, containing font characters etc. /// - /// Not valid until first call to [`CtxRef::run()`]. + /// Not valid until first call to [`Context::run()`]. /// That's because since we don't know the proper `pixels_per_point` until then. pub fn font_image(&self) -> Arc { self.fonts().font_image() @@ -483,7 +507,7 @@ impl Context { /// /// The new fonts will become active at the start of the next frame. pub fn set_fonts(&self, font_definitions: FontDefinitions) { - if let Some(current_fonts) = &self.fonts { + if let Some(current_fonts) = &*self.fonts_mut() { // NOTE: this comparison is expensive since it checks TTF data for equality if current_fonts.definitions() == &font_definitions { return; // no change - save us from reloading font textures @@ -504,7 +528,7 @@ impl Context { /// /// Example: /// ``` - /// # let mut ctx = egui::CtxRef::default(); + /// # let mut ctx = egui::Context::default(); /// let mut style: egui::Style = (*ctx.style()).clone(); /// style.spacing.item_spacing = egui::vec2(10.0, 20.0); /// ctx.set_style(style); @@ -519,7 +543,7 @@ impl Context { /// /// Example: /// ``` - /// # let mut ctx = egui::CtxRef::default(); + /// # let mut ctx = egui::Context::default(); /// ctx.set_visuals(egui::Visuals::light()); // Switch to light mode /// ``` pub fn set_visuals(&self, visuals: crate::Visuals) { @@ -529,7 +553,7 @@ impl Context { /// The number of physical pixels for each logical point. #[inline(always)] pub fn pixels_per_point(&self) -> f32 { - self.input.pixels_per_point() + self.input().pixels_per_point() } /// Set the number of physical pixels for each logical point. @@ -604,75 +628,30 @@ impl Context { Rect::from_min_size(pos, window.size()) } +} - // --------------------------------------------------------------------- - - fn begin_frame_mut(&mut self, new_raw_input: RawInput) { - self.memory().begin_frame(&self.input, &new_raw_input); - - let mut input = std::mem::take(&mut self.input); - if let Some(new_pixels_per_point) = self.memory().new_pixels_per_point.take() { - input.pixels_per_point = new_pixels_per_point; - } - - self.input = input.begin_frame(new_raw_input); - self.frame_state.lock().begin_frame(&self.input); - - self.update_fonts(self.input.pixels_per_point()); - - // Ensure we register the background area so panels and background ui can catch clicks: - let screen_rect = self.input.screen_rect(); - self.memory().areas.set_state( - LayerId::background(), - containers::area::State { - pos: screen_rect.min, - size: screen_rect.size(), - interactable: true, - }, - ); - } - - /// Load fonts unless already loaded. - fn update_fonts(&mut self, pixels_per_point: f32) { - let new_font_definitions = self.memory().new_font_definitions.take(); - - let pixels_per_point_changed = match &self.fonts { - None => true, - Some(current_fonts) => { - (current_fonts.pixels_per_point() - pixels_per_point).abs() > 1e-3 - } - }; - - if self.fonts.is_none() || new_font_definitions.is_some() || pixels_per_point_changed { - self.fonts = Some(Arc::new(Fonts::new( - pixels_per_point, - new_font_definitions.unwrap_or_else(|| { - self.fonts - .as_ref() - .map(|font| font.definitions().clone()) - .unwrap_or_default() - }), - ))); - } - } - +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) { - if self.input.wants_repaint() { + if self.input().wants_repaint() { self.request_repaint(); } - self.memory() - .end_frame(&self.input, &self.frame_state().used_ids); + { + let ctx_impl = &mut *self.write(); + ctx_impl + .memory + .end_frame(&ctx_impl.input, &ctx_impl.frame_state.used_ids); + } self.fonts().end_frame(); let mut output: Output = std::mem::take(&mut self.output()); - if self.repaint_requests.load(SeqCst) > 0 { - self.repaint_requests.fetch_sub(1, SeqCst); + if self.read().repaint_requests > 0 { + self.write().repaint_requests -= 1; output.needs_repaint = true; } @@ -681,8 +660,11 @@ impl Context { } fn drain_paint_lists(&self) -> Vec { - let memory = self.memory(); - self.graphics().drain(memory.areas.order()).collect() + let ctx_impl = &mut *self.write(); + ctx_impl + .graphics + .drain(ctx_impl.memory.areas.order()) + .collect() } /// Tessellate the given shapes into triangle meshes. @@ -700,7 +682,7 @@ impl Context { tessellation_options, self.fonts().font_image().size(), ); - *self.paint_stats.lock() = paint_stats.with_clipped_meshes(&clipped_meshes); + self.write().paint_stats = paint_stats.with_clipped_meshes(&clipped_meshes); clipped_meshes } @@ -725,7 +707,8 @@ impl Context { /// Is the pointer (mouse/touch) over any egui area? pub fn is_pointer_over_area(&self) -> bool { - if let Some(pointer_pos) = self.input.pointer.interact_pos() { + let pointer_pos = self.input().pointer.interact_pos(); + if let Some(pointer_pos) = pointer_pos { if let Some(layer) = self.layer_id_at(pointer_pos) { if layer.order == Order::Background { !self.frame_state().unused_rect.contains(pointer_pos) @@ -759,14 +742,45 @@ impl Context { pub fn wants_keyboard_input(&self) -> bool { self.memory().interaction.focus.focused().is_some() } +} - // --------------------------------------------------------------------- +// Ergonomic methods to forward some calls often used in 'if let' without holding the borrow +impl Context { + /// Latest reported pointer position. + /// When tapping a touch screen, this will be `None`. + #[inline(always)] + pub(crate) fn latest_pointer_pos(&self) -> Option { + self.input().pointer.latest_pos() + } + /// If it is a good idea to show a tooltip, where is pointer? + #[inline(always)] + pub fn pointer_hover_pos(&self) -> Option { + self.input().pointer.hover_pos() + } + + /// If you detect a click or drag and wants to know where it happened, use this. + /// + /// Latest position of the mouse, but ignoring any [`Event::PointerGone`] + /// if there were interactions this frame. + /// When tapping a touch screen, this will be the location of the touch. + #[inline(always)] + pub fn pointer_interact_pos(&self) -> Option { + self.input().pointer.interact_pos() + } + + /// Calls [`InputState::multi_touch`]. + pub fn multi_touch(&self) -> Option { + self.input().multi_touch() + } +} + +impl Context { /// Move all the graphics at the given layer. /// Can be used to implement drag-and-drop (see relevant demo). pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) { if delta != Vec2::ZERO { - self.graphics().list(layer_id).lock().translate(delta); + self.graphics().list(layer_id).translate(delta); } } @@ -777,7 +791,8 @@ impl Context { } pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { - if let Some(pointer_pos) = self.input.pointer.interact_pos() { + let pointer_pos = self.input().pointer.interact_pos(); + if let Some(pointer_pos) = pointer_pos { rect.contains(pointer_pos) && self.layer_id_at(pointer_pos) == Some(layer_id) } else { false @@ -817,10 +832,12 @@ impl Context { /// Like [`Self::animate_bool`] but allows you to control the animation time. pub fn animate_bool_with_time(&self, id: Id, value: bool, animation_time: f32) -> f32 { - let animated_value = - self.animation_manager - .lock() - .animate_bool(&self.input, animation_time, id, value); + let animated_value = { + let ctx_impl = &mut *self.write(); + ctx_impl + .animation_manager + .animate_bool(&ctx_impl.input, animation_time, id, value) + }; let animation_in_progress = 0.0 < animated_value && animated_value < 1.0; if animation_in_progress { self.request_repaint(); @@ -830,7 +847,7 @@ impl Context { /// Clear memory of any animations. pub fn clear_animations(&self) { - *self.animation_manager.lock() = Default::default(); + self.write().animation_manager = Default::default(); } } @@ -849,7 +866,8 @@ impl Context { .show(ui, |ui| { let mut font_definitions = self.fonts().definitions().clone(); font_definitions.ui(ui); - self.fonts().font_image().ui(ui); + let font_image = self.fonts().font_image(); + font_image.ui(ui); self.set_fonts(font_definitions); }); @@ -891,16 +909,12 @@ impl Context { .on_hover_text("Is egui currently listening for text input?"); let pointer_pos = self - .input() - .pointer - .hover_pos() + .pointer_hover_pos() .map_or_else(String::new, |pos| format!("{:?}", pos)); ui.label(format!("Pointer pos: {}", pointer_pos)); let top_layer = self - .input() - .pointer - .hover_pos() + .pointer_hover_pos() .and_then(|pos| self.layer_id_at(pos)) .map_or_else(String::new, |layer| layer.short_debug_format()); ui.label(format!("Top layer under mouse: {}", top_layer)); @@ -916,12 +930,16 @@ impl Context { CollapsingHeader::new("📥 Input") .default_open(false) - .show(ui, |ui| ui.input().clone().ui(ui)); + .show(ui, |ui| { + let input = ui.input().clone(); + input.ui(ui); + }); CollapsingHeader::new("📊 Paint stats") .default_open(true) .show(ui, |ui| { - self.paint_stats.lock().ui(ui); + let paint_stats = self.write().paint_stats; + paint_stats.ui(ui); }); } diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index a66c557ea3a..2a5511b936a 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -135,7 +135,7 @@ pub struct DroppedFile { /// Set by the `egui_web` backend. pub last_modified: Option, /// Set by the `egui_web` backend. - pub bytes: Option>, + pub bytes: Option>, } /// An input event generated by the integration. diff --git a/egui/src/frame_state.rs b/egui/src/frame_state.rs index c2f03aebb0b..848dc41597b 100644 --- a/egui/src/frame_state.rs +++ b/egui/src/frame_state.rs @@ -72,7 +72,7 @@ impl FrameState { pub(crate) fn available_rect(&self) -> Rect { crate::egui_assert!( self.available_rect.is_finite(), - "Called `available_rect()` before `CtxRef::run()`" + "Called `available_rect()` before `Context::run()`" ); self.available_rect } diff --git a/egui/src/grid.rs b/egui/src/grid.rs index 2aba3ca63a6..281377fe28a 100644 --- a/egui/src/grid.rs +++ b/egui/src/grid.rs @@ -45,8 +45,8 @@ impl State { // ---------------------------------------------------------------------------- pub(crate) struct GridLayout { - ctx: CtxRef, - style: std::sync::Arc