diff --git a/.gitignore b/.gitignore index f33c23b9730..c43b95594e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ +.DS_Store **/target **/target_ra +**/target_wasm /.*.json /.vscode /media/* -.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e3405963d5..dbd28bf1a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ## Unreleased +* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)): + * `ctx.input().key_pressed(Key::A)` -> `ctx.input(|i| i.key_pressed(Key::A))` + * `ui.memory().toggle_popup(popup_id)` -> `ui.memory_mut(|mem| mem.toggle_popup(popup_id))` + ### Added ⭐ * Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)). * Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)). @@ -18,6 +22,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)). * Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)). * Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)). +* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)). ### Changed πŸ”§ * Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). diff --git a/Cargo.lock b/Cargo.lock index 36a4872b871..34cc098fba7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2171,6 +2171,13 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "hello_world_par" +version = "0.1.0" +dependencies = [ + "eframe", +] + [[package]] name = "hermit-abi" version = "0.1.19" diff --git a/README.md b/README.md index 55c5feae07b..e30042a95e9 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,8 @@ egui uses the builder pattern for construction widgets. For instance: `ui.add(La Instead of using matching `begin/end` style function calls (which can be error prone) egui prefers to use `FnOnce` closures passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this. More discussion of this at . +egui uses a single `RwLock` for short-time locks on each access of `Context` data. This is to leave implementation simple and transactional and allow users to run their UI logic in parallel. Instead of creating mutex guards, egui uses closures passed to a wrapping function, e.g. `ctx.input(|i| i.key_down(Key::A))`. This is to make it less likely that a user would accidentally double-lock the `Context`, which would lead to a deadlock. + ### Inspiration The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code and turned GUI programming from something I hated to do to something I now enjoy. @@ -396,6 +398,7 @@ Notable contributions by: * [@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). +* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625). * 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/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index ef528e40b11..e7fb4935546 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -55,7 +55,7 @@ persistence = [ ## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"] -## Enable screen reader support (requires `ctx.options().screen_reader = true;`) +## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) screen_reader = ["egui-winit/screen_reader", "tts"] ## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit. diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index e6442736b0f..c4bd540640d 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -173,7 +173,7 @@ pub trait App { } /// If `true` a warm-up call to [`Self::update`] will be issued where - /// `ctx.memory().everything_is_visible()` will be set to `true`. + /// `ctx.memory(|mem| mem.everything_is_visible())` will be set to `true`. /// /// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on. /// diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index ff54d12f9eb..ea572271cc4 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -257,7 +257,8 @@ impl EpiIntegration { ) -> Self { let egui_ctx = egui::Context::default(); - *egui_ctx.memory() = load_egui_memory(storage.as_deref()).unwrap_or_default(); + let memory = load_egui_memory(storage.as_deref()).unwrap_or_default(); + egui_ctx.memory_mut(|mem| *mem = memory); let native_pixels_per_point = window.scale_factor() as f32; @@ -315,11 +316,12 @@ impl EpiIntegration { pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { crate::profile_function!(); - let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); - self.egui_ctx.memory().set_everything_is_visible(true); + let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone()); + self.egui_ctx + .memory_mut(|mem| mem.set_everything_is_visible(true)); let full_output = self.update(app, 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.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge. self.egui_ctx.clear_animations(); } @@ -446,7 +448,8 @@ impl EpiIntegration { } if _app.persist_egui_memory() { crate::profile_scope!("egui_memory"); - epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory()); + self.egui_ctx + .memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem)); } { crate::profile_scope!("App::save"); diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index f139cab7f2f..107a8ce44ce 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -313,10 +313,11 @@ impl AppRunner { pub fn warm_up(&mut self) -> Result<(), JsValue> { if self.app.warm_up_enabled() { - let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); - self.egui_ctx.memory().set_everything_is_visible(true); + let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone()); + self.egui_ctx + .memory_mut(|m| m.set_everything_is_visible(true)); self.logic()?; - *self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge. + self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge. self.egui_ctx.clear_animations(); } Ok(()) @@ -388,7 +389,7 @@ impl AppRunner { } fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { - if self.egui_ctx.options().screen_reader { + if self.egui_ctx.options(|o| o.screen_reader) { self.screen_reader .speak(&platform_output.events_description()); } diff --git a/crates/eframe/src/web/storage.rs b/crates/eframe/src/web/storage.rs index 6a3d5279407..f0f3c843666 100644 --- a/crates/eframe/src/web/storage.rs +++ b/crates/eframe/src/web/storage.rs @@ -15,7 +15,7 @@ pub fn load_memory(ctx: &egui::Context) { if let Some(memory_string) = local_storage_get("egui_memory_ron") { match ron::from_str(&memory_string) { Ok(memory) => { - *ctx.memory() = memory; + ctx.memory_mut(|m| *m = memory); } Err(err) => { tracing::error!("Failed to parse memory RON: {}", err); @@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {} #[cfg(feature = "persistence")] pub fn save_memory(ctx: &egui::Context) { - match ron::to_string(&*ctx.memory()) { + match ctx.memory(|mem| ron::to_string(mem)) { Ok(ron) => { local_storage_set("egui_memory_ron", &ron); } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 82c2d68c846..07c5766e254 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -615,7 +615,7 @@ impl State { egui_ctx: &egui::Context, platform_output: egui::PlatformOutput, ) { - if egui_ctx.options().screen_reader { + if egui_ctx.options(|o| o.screen_reader) { self.screen_reader .speak(&platform_output.events_description()); } diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index de348e15ac0..84f0ea44632 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -5,7 +5,7 @@ use crate::*; /// State that is persisted between frames. -// TODO(emilk): this is not currently stored in `memory().data`, but maybe it should be? +// TODO(emilk): this is not currently stored in `Memory::data`, but maybe it should be? #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct State { @@ -231,7 +231,7 @@ impl Area { let layer_id = LayerId::new(order, id); - let state = ctx.memory().areas.get(id).copied(); + let state = ctx.memory(|mem| mem.areas.get(id).copied()); let is_new = state.is_none(); if is_new { ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place @@ -278,7 +278,7 @@ impl Area { // Important check - don't try to move e.g. a combobox popup! if movable { if move_response.dragged() { - state.pos += ctx.input().pointer.delta(); + state.pos += ctx.input(|i| i.pointer.delta()); } state.pos = ctx @@ -288,9 +288,9 @@ impl Area { if (move_response.dragged() || move_response.clicked()) || pointer_pressed_on_area(ctx, layer_id) - || !ctx.memory().areas.visible_last_frame(&layer_id) + || !ctx.memory(|m| m.areas.visible_last_frame(&layer_id)) { - ctx.memory().areas.move_to_top(layer_id); + ctx.memory_mut(|m| m.areas.move_to_top(layer_id)); ctx.request_repaint(); } @@ -329,7 +329,7 @@ impl Area { } let layer_id = LayerId::new(self.order, self.id); - let area_rect = ctx.memory().areas.get(self.id).map(|area| area.rect()); + let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect())); if let Some(area_rect) = area_rect { let clip_rect = ctx.available_rect(); let painter = Painter::new(ctx.clone(), layer_id, clip_rect); @@ -358,7 +358,7 @@ impl Prepared { } pub(crate) fn content_ui(&self, ctx: &Context) -> Ui { - let screen_rect = ctx.input().screen_rect(); + let screen_rect = ctx.screen_rect(); let bounds = if let Some(bounds) = self.drag_bounds { bounds.intersect(screen_rect) // protect against infinite bounds @@ -410,7 +410,7 @@ impl Prepared { state.size = content_ui.min_rect().size(); - ctx.memory().areas.set_state(layer_id, state); + ctx.memory_mut(|m| m.areas.set_state(layer_id, state)); move_response } @@ -418,7 +418,7 @@ impl Prepared { fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { if let Some(pointer_pos) = ctx.pointer_interact_pos() { - let any_pressed = ctx.input().pointer.any_pressed(); + let any_pressed = ctx.input(|i| i.pointer.any_pressed()); any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id) } else { false @@ -426,13 +426,13 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { } fn automatic_area_position(ctx: &Context) -> Pos2 { - let mut existing: Vec = ctx - .memory() - .areas - .visible_windows() - .into_iter() - .map(State::rect) - .collect(); + let mut existing: Vec = ctx.memory(|mem| { + mem.areas + .visible_windows() + .into_iter() + .map(State::rect) + .collect() + }); existing.sort_by_key(|r| r.left().round() as i32); let available_rect = ctx.available_rect(); diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index ea7d52dedeb..c8803fc7e2d 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -26,13 +26,14 @@ pub struct CollapsingState { impl CollapsingState { pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data() - .get_persisted::(id) - .map(|state| Self { id, state }) + ctx.data_mut(|d| { + d.get_persisted::(id) + .map(|state| Self { id, state }) + }) } pub fn store(&self, ctx: &Context) { - ctx.data().insert_persisted(self.id, self.state); + ctx.data_mut(|d| d.insert_persisted(self.id, self.state)); } pub fn id(&self) -> Id { @@ -64,7 +65,7 @@ impl CollapsingState { /// 0 for closed, 1 for open, with tweening pub fn openness(&self, ctx: &Context) -> f32 { - if ctx.memory().everything_is_visible() { + if ctx.memory(|mem| mem.everything_is_visible()) { 1.0 } else { ctx.animate_bool(self.id, self.state.open) diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 911f9424279..8ede4d670a9 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -242,18 +242,13 @@ fn combo_box_dyn<'c, R>( ) -> InnerResponse> { let popup_id = button_id.with("popup"); - let is_popup_open = ui.memory().is_popup_open(popup_id); + let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id)); - let popup_height = ui - .ctx() - .memory() - .areas - .get(popup_id) - .map_or(100.0, |state| state.size.y); + let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y)); let above_or_below = if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height - < ui.ctx().input().screen_rect().bottom() + < ui.ctx().screen_rect().bottom() { AboveOrBelow::Below } else { @@ -334,7 +329,7 @@ fn combo_box_dyn<'c, R>( }); if button_response.clicked() { - ui.memory().toggle_popup(popup_id); + ui.memory_mut(|mem| mem.toggle_popup(popup_id)); } let inner = crate::popup::popup_above_or_below_widget( ui, diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index e4947db39a5..959ef595db0 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -28,7 +28,7 @@ pub struct PanelState { impl PanelState { pub fn load(ctx: &Context, bar_id: Id) -> Option { - ctx.data().get_persisted(bar_id) + ctx.data_mut(|d| d.get_persisted(bar_id)) } /// The size of the panel (from previous frame). @@ -37,7 +37,7 @@ impl PanelState { } fn store(self, ctx: &Context, bar_id: Id) { - ctx.data().insert_persisted(bar_id, self); + ctx.data_mut(|d| d.insert_persisted(bar_id, self)); } } @@ -245,11 +245,12 @@ impl SidePanel { && (resize_x - pointer.x).abs() <= ui.style().interaction.resize_grab_radius_side; - let any_pressed = ui.input().pointer.any_pressed(); // avoid deadlocks - if any_pressed && ui.input().pointer.any_down() && mouse_over_resize_line { - ui.memory().set_dragged_id(resize_id); + if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) + && mouse_over_resize_line + { + ui.memory_mut(|mem| mem.set_dragged_id(resize_id)); } - is_resizing = ui.memory().is_being_dragged(resize_id); + is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id)); if is_resizing { let width = (pointer.x - side.side_x(panel_rect)).abs(); let width = @@ -257,12 +258,12 @@ impl SidePanel { side.set_rect_width(&mut panel_rect, width); } - let any_down = ui.input().pointer.any_down(); // avoid deadlocks - let dragging_something_else = any_down || ui.input().pointer.any_pressed(); + let dragging_something_else = + ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); resize_hover = mouse_over_resize_line && !dragging_something_else; if resize_hover || is_resizing { - ui.output().cursor_icon = CursorIcon::ResizeHorizontal; + ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); } } } @@ -334,19 +335,19 @@ impl SidePanel { let layer_id = LayerId::background(); let side = self.side; let available_rect = ctx.available_rect(); - let clip_rect = ctx.input().screen_rect(); + let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; match side { - Side::Left => ctx - .frame_state() - .allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)), - Side::Right => ctx - .frame_state() - .allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)), + Side::Left => ctx.frame_state_mut(|state| { + state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)); + }), + Side::Right => ctx.frame_state_mut(|state| { + state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)); + }), } inner_response } @@ -682,7 +683,7 @@ impl TopBottomPanel { let mut is_resizing = false; if resizable { let resize_id = id.with("__resize"); - let latest_pos = ui.input().pointer.latest_pos(); + let latest_pos = ui.input(|i| i.pointer.latest_pos()); if let Some(pointer) = latest_pos { let we_are_on_top = ui .ctx() @@ -695,13 +696,12 @@ impl TopBottomPanel { && (resize_y - pointer.y).abs() <= ui.style().interaction.resize_grab_radius_side; - if ui.input().pointer.any_pressed() - && ui.input().pointer.any_down() + if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) && mouse_over_resize_line { - ui.memory().interaction.drag_id = Some(resize_id); + ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id)); } - is_resizing = ui.memory().interaction.drag_id == Some(resize_id); + is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id)); if is_resizing { let height = (pointer.y - side.side_y(panel_rect)).abs(); let height = clamp_to_range(height, height_range.clone()) @@ -709,12 +709,12 @@ impl TopBottomPanel { side.set_rect_height(&mut panel_rect, height); } - let any_down = ui.input().pointer.any_down(); // avoid deadlocks - let dragging_something_else = any_down || ui.input().pointer.any_pressed(); + let dragging_something_else = + ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); resize_hover = mouse_over_resize_line && !dragging_something_else; if resize_hover || is_resizing { - ui.output().cursor_icon = CursorIcon::ResizeVertical; + ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical); } } } @@ -787,7 +787,7 @@ impl TopBottomPanel { let available_rect = ctx.available_rect(); let side = self.side; - let clip_rect = ctx.input().screen_rect(); + let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); @@ -795,12 +795,14 @@ impl TopBottomPanel { match side { TopBottomSide::Top => { - ctx.frame_state() - .allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max)); + ctx.frame_state_mut(|state| { + state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max)); + }); } TopBottomSide::Bottom => { - ctx.frame_state() - .allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max)); + ctx.frame_state_mut(|state| { + state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max)); + }); } } @@ -1042,14 +1044,13 @@ impl CentralPanel { let layer_id = LayerId::background(); let id = Id::new("central_panel"); - let clip_rect = ctx.input().screen_rect(); + let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_rect); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); // Only inform ctx about what we actually used, so we can shrink the native window to fit. - ctx.frame_state() - .allocate_central_panel(inner_response.response.rect); + ctx.frame_state_mut(|state| state.allocate_central_panel(inner_response.response.rect)); inner_response } diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index add02b22b51..adb31ce6906 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -13,11 +13,11 @@ pub(crate) struct TooltipState { impl TooltipState { pub fn load(ctx: &Context) -> Option { - ctx.data().get_temp(Id::null()) + ctx.data_mut(|d| d.get_temp(Id::null())) } fn store(self, ctx: &Context) { - ctx.data().insert_temp(Id::null(), self); + ctx.data_mut(|d| d.insert_temp(Id::null(), self)); } fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option { @@ -95,9 +95,7 @@ pub fn show_tooltip_at_pointer( add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { let suggested_pos = ctx - .input() - .pointer - .hover_pos() + .input(|i| i.pointer.hover_pos()) .map(|pointer_pos| pointer_pos + vec2(16.0, 16.0)); show_tooltip_at(ctx, id, suggested_pos, add_contents) } @@ -112,7 +110,7 @@ pub fn show_tooltip_for( add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { let expanded_rect = rect.expand2(vec2(2.0, 4.0)); - let (above, position) = if ctx.input().any_touches() { + let (above, position) = if ctx.input(|i| i.any_touches()) { (true, expanded_rect.left_top()) } else { (false, expanded_rect.left_bottom()) @@ -159,8 +157,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( // if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work. let mut frame_state = - ctx.frame_state() - .tooltip_state + ctx.frame_state(|fs| fs.tooltip_state) .unwrap_or(crate::frame_state::TooltipFrameState { common_id: individual_id, rect: Rect::NOTHING, @@ -176,7 +173,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( } } else if let Some(position) = suggested_position { position - } else if ctx.memory().everything_is_visible() { + } else if ctx.memory(|mem| mem.everything_is_visible()) { Pos2::ZERO } else { return None; // No good place for a tooltip :( @@ -191,7 +188,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( position.y -= expected_size.y; } - position = position.at_most(ctx.input().screen_rect().max - expected_size); + position = position.at_most(ctx.screen_rect().max - expected_size); // check if we intersect the avoid_rect { @@ -209,7 +206,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( } } - let position = position.at_least(ctx.input().screen_rect().min); + let position = position.at_least(ctx.screen_rect().min); let area_id = frame_state.common_id.with(frame_state.count); @@ -226,7 +223,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( frame_state.count += 1; frame_state.rect = frame_state.rect.union(response.rect); - ctx.frame_state().tooltip_state = Some(frame_state); + ctx.frame_state_mut(|fs| fs.tooltip_state = Some(frame_state)); Some(inner) } @@ -283,7 +280,7 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool { if *individual_id == tooltip_id { let area_id = common_id.with(count); let layer_id = LayerId::new(Order::Tooltip, area_id); - if ctx.memory().areas.visible_last_frame(&layer_id) { + if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) { return true; } } @@ -325,7 +322,7 @@ pub fn popup_below_widget( /// let response = ui.button("Open popup"); /// let popup_id = ui.make_persistent_id("my_unique_id"); /// if response.clicked() { -/// ui.memory().toggle_popup(popup_id); +/// ui.memory_mut(|mem| mem.toggle_popup(popup_id)); /// } /// let below = egui::AboveOrBelow::Below; /// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| { @@ -342,7 +339,7 @@ pub fn popup_above_or_below_widget( above_or_below: AboveOrBelow, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - if ui.memory().is_popup_open(popup_id) { + if ui.memory(|mem| mem.is_popup_open(popup_id)) { let (pos, pivot) = match above_or_below { AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), @@ -370,8 +367,8 @@ pub fn popup_above_or_below_widget( }) .inner; - if ui.input().key_pressed(Key::Escape) || widget_response.clicked_elsewhere() { - ui.memory().close_popup(); + if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { + ui.memory_mut(|mem| mem.close_popup()); } Some(inner) } else { diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 844fc758486..befb51a109a 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -18,11 +18,11 @@ pub(crate) struct State { impl State { pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data().get_persisted(id) + ctx.data_mut(|d| d.get_persisted(id)) } pub fn store(self, ctx: &Context, id: Id) { - ctx.data().insert_persisted(id, self); + ctx.data_mut(|d| d.insert_persisted(id, self)); } } @@ -180,7 +180,7 @@ impl Resize { .at_least(self.min_size) .at_most(self.max_size) .at_most( - ui.input().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows + ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows ); State { @@ -305,7 +305,7 @@ impl Resize { paint_resize_corner(ui, &corner_response); if corner_response.hovered() || corner_response.dragged() { - ui.ctx().output().cursor_icon = CursorIcon::ResizeNwSe; + ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe); } } diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 7af37e40ce7..80158f52d30 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -48,11 +48,11 @@ impl Default for State { impl State { pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data().get_persisted(id) + ctx.data_mut(|d| d.get_persisted(id)) } pub fn store(self, ctx: &Context, id: Id) { - ctx.data().insert_persisted(id, self); + ctx.data_mut(|d| d.insert_persisted(id, self)); } } @@ -449,8 +449,10 @@ impl ScrollArea { if content_response.dragged() { for d in 0..2 { if has_bar[d] { - state.offset[d] -= ui.input().pointer.delta()[d]; - state.vel[d] = ui.input().pointer.velocity()[d]; + ui.input(|input| { + state.offset[d] -= input.pointer.delta()[d]; + state.vel[d] = input.pointer.velocity()[d]; + }); state.scroll_stuck_to_end[d] = false; } else { state.vel[d] = 0.0; @@ -459,7 +461,7 @@ impl ScrollArea { } else { let stop_speed = 20.0; // Pixels per second. let friction_coeff = 1000.0; // Pixels per second squared. - let dt = ui.input().unstable_dt; + let dt = ui.input(|i| i.unstable_dt); let friction = friction_coeff * dt; if friction > state.vel.length() || state.vel.length() < stop_speed { @@ -603,7 +605,9 @@ impl Prepared { for d in 0..2 { if has_bar[d] { // We take the scroll target so only this ScrollArea will use it: - let scroll_target = content_ui.ctx().frame_state().scroll_target[d].take(); + let scroll_target = content_ui + .ctx() + .frame_state_mut(|state| state.scroll_target[d].take()); if let Some((scroll, align)) = scroll_target { let min = content_ui.min_rect().min[d]; let clip_rect = content_ui.clip_rect(); @@ -668,8 +672,7 @@ impl Prepared { if scrolling_enabled && ui.rect_contains_pointer(outer_rect) { for d in 0..2 { if has_bar[d] { - let mut frame_state = ui.ctx().frame_state(); - let scroll_delta = frame_state.scroll_delta; + let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta); let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0; let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0; @@ -677,7 +680,7 @@ impl Prepared { if scrolling_up || scrolling_down { state.offset[d] -= scroll_delta[d]; // Clear scroll delta so no parent scroll will use it. - frame_state.scroll_delta[d] = 0.0; + ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0); state.scroll_stuck_to_end[d] = false; } } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 1182e6a7314..a236a5e3d98 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -301,7 +301,8 @@ impl<'open> Window<'open> { let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); - let is_open = !matches!(open, Some(false)) || ctx.memory().everything_is_visible(); + let is_explicitly_closed = matches!(open, Some(false)); + let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); area.show_open_close_animation(ctx, &frame, is_open); if !is_open { @@ -339,7 +340,7 @@ impl<'open> Window<'open> { // Calculate roughly how much larger the window size is compared to the inner rect let title_bar_height = if with_title_bar { let style = ctx.style(); - title.font_height(&ctx.fonts(), &style) + title_content_spacing + ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing } else { 0.0 }; @@ -425,7 +426,7 @@ impl<'open> Window<'open> { ctx.style().visuals.widgets.active, ); } else if let Some(hover_interaction) = hover_interaction { - if ctx.input().pointer.has_pointer() { + if ctx.input(|i| i.pointer.has_pointer()) { paint_frame_interaction( &mut area_content_ui, outer_rect, @@ -520,13 +521,13 @@ pub(crate) struct WindowInteraction { impl WindowInteraction { pub fn set_cursor(&self, ctx: &Context) { if (self.left && self.top) || (self.right && self.bottom) { - ctx.output().cursor_icon = CursorIcon::ResizeNwSe; + ctx.set_cursor_icon(CursorIcon::ResizeNwSe); } else if (self.right && self.top) || (self.left && self.bottom) { - ctx.output().cursor_icon = CursorIcon::ResizeNeSw; + ctx.set_cursor_icon(CursorIcon::ResizeNeSw); } else if self.left || self.right { - ctx.output().cursor_icon = CursorIcon::ResizeHorizontal; + ctx.set_cursor_icon(CursorIcon::ResizeHorizontal); } else if self.bottom || self.top { - ctx.output().cursor_icon = CursorIcon::ResizeVertical; + ctx.set_cursor_icon(CursorIcon::ResizeVertical); } } @@ -558,7 +559,7 @@ fn interact( } } - ctx.memory().areas.move_to_top(area_layer_id); + ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id)); Some(window_interaction) } @@ -566,11 +567,11 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction) window_interaction.set_cursor(ctx); // Only move/resize windows with primary mouse button: - if !ctx.input().pointer.primary_down() { + if !ctx.input(|i| i.pointer.primary_down()) { return None; } - let pointer_pos = ctx.input().pointer.interact_pos()?; + let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?; let mut rect = window_interaction.start_rect; // prevent drift if window_interaction.is_resize() { @@ -592,8 +593,8 @@ fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction) // but we want anything interactive in the window (e.g. slider) to steal // the drag from us. It is therefor important not to move the window the first frame, // but instead let other widgets to the steal. HACK. - if !ctx.input().pointer.any_pressed() { - let press_origin = ctx.input().pointer.press_origin()?; + if !ctx.input(|i| i.pointer.any_pressed()) { + let press_origin = ctx.input(|i| i.pointer.press_origin())?; let delta = pointer_pos - press_origin; rect = rect.translate(delta); } @@ -611,30 +612,31 @@ fn window_interaction( rect: Rect, ) -> Option { { - let drag_id = ctx.memory().interaction.drag_id; + let drag_id = ctx.memory(|mem| mem.interaction.drag_id); if drag_id.is_some() && drag_id != Some(id) { return None; } } - let mut window_interaction = { ctx.memory().window_interaction }; + let mut window_interaction = ctx.memory(|mem| mem.window_interaction); if window_interaction.is_none() { if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { hover_window_interaction.set_cursor(ctx); - let any_pressed = ctx.input().pointer.any_pressed(); // avoid deadlocks - if any_pressed && ctx.input().pointer.primary_down() { - ctx.memory().interaction.drag_id = Some(id); - ctx.memory().interaction.drag_is_window = true; - window_interaction = Some(hover_window_interaction); - ctx.memory().window_interaction = window_interaction; + if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) { + ctx.memory_mut(|mem| { + mem.interaction.drag_id = Some(id); + mem.interaction.drag_is_window = true; + window_interaction = Some(hover_window_interaction); + mem.window_interaction = window_interaction; + }); } } } if let Some(window_interaction) = window_interaction { - let is_active = ctx.memory().interaction.drag_id == Some(id); + let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id)); if is_active && window_interaction.area_layer_id == area_layer_id { return Some(window_interaction); @@ -650,10 +652,9 @@ fn resize_hover( area_layer_id: LayerId, rect: Rect, ) -> Option { - let pointer = ctx.input().pointer.interact_pos()?; + let pointer = ctx.input(|i| i.pointer.interact_pos())?; - let any_down = ctx.input().pointer.any_down(); // avoid deadlocks - if any_down && !ctx.input().pointer.any_pressed() { + if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) { return None; // already dragging (something) } @@ -663,7 +664,7 @@ fn resize_hover( } } - if ctx.memory().interaction.drag_interest { + if ctx.memory(|mem| mem.interaction.drag_interest) { // Another widget will become active if we drag here return None; } @@ -825,8 +826,8 @@ fn show_title_bar( collapsible: bool, ) -> TitleBar { let inner_response = ui.horizontal(|ui| { - let height = title - .font_height(&ui.fonts(), ui.style()) + let height = ui + .fonts(|fonts| title.font_height(fonts, ui.style())) .max(ui.spacing().interact_size.y); ui.set_min_height(height); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index d94f9b79087..f42690ee7c6 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use crate::{ animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState, input_state::*, layers::GraphicLayers, memory::Options, os::OperatingSystem, - output::FullOutput, TextureHandle, *, + output::FullOutput, util::IdTypeMap, TextureHandle, *, }; use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; @@ -179,12 +179,26 @@ impl ContextImpl { /// [`Context`] is cheap to clone, and any clones refers to the same mutable data /// ([`Context`] uses refcounting internally). /// -/// All methods are marked `&self`; [`Context`] has interior mutability (protected by a mutex). +/// ## Locking +/// All methods are marked `&self`; [`Context`] has interior mutability protected by an [`RwLock`]. /// +/// To access parts of a `Context` you need to use some of the helper functions that take closures: /// -/// You can store +/// ``` +/// # let ctx = egui::Context::default(); +/// if ctx.input(|i| i.key_pressed(egui::Key::A)) { +/// ctx.output_mut(|o| o.copied_text = "Hello!".to_string()); +/// } +/// ``` +/// +/// Within such a closure you may NOT recursively lock the same [`Context`], as that can lead to a deadlock. +/// Therefore it is important that any lock of [`Context`] is short-lived. +/// +/// These are effectively transactional accesses. /// -/// # Example: +/// [`Ui`] has many of the same accessor functions, and the same applies there. +/// +/// ## Example: /// /// ``` no_run /// # fn handle_platform_output(_: egui::PlatformOutput) {} @@ -234,12 +248,14 @@ impl Default for Context { } impl Context { - fn read(&self) -> RwLockReadGuard<'_, ContextImpl> { - self.0.read() + // Do read-only (shared access) transaction on Context + fn read(&self, reader: impl FnOnce(&ContextImpl) -> R) -> R { + reader(&self.0.read()) } - fn write(&self) -> RwLockWriteGuard<'_, ContextImpl> { - self.0.write() + // Do read-write (exclusive access) transaction on Context + fn write(&self, writer: impl FnOnce(&mut ContextImpl) -> R) -> R { + writer(&mut self.0.write()) } /// Run the ui code for one frame. @@ -289,9 +305,150 @@ impl Context { /// // handle full_output /// ``` pub fn begin_frame(&self, new_input: RawInput) { - self.write().begin_frame_mut(new_input); + self.write(|ctx| ctx.begin_frame_mut(new_input)); + } +} + +/// ## Borrows parts of [`Context`] +/// These functions all lock the [`Context`]. +/// Please see the documentation of [`Context`] for how locking works! +impl Context { + /// Read-only access to [`InputState`]. + /// + /// Note that this locks the [`Context`]. + /// + /// ``` + /// # let mut ctx = egui::Context::default(); + /// ctx.input(|i| { + /// // ⚠️ Using `ctx` (even from other `Arc` reference) again here will lead to a dead-lock! + /// }); + /// + /// if let Some(pos) = ctx.input(|i| i.pointer.hover_pos()) { + /// // This is fine! + /// } + /// ``` + #[inline] + pub fn input(&self, reader: impl FnOnce(&InputState) -> R) -> R { + self.read(move |ctx| reader(&ctx.input)) + } + + /// Read-write access to [`InputState`]. + #[inline] + pub fn input_mut(&self, writer: impl FnOnce(&mut InputState) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.input)) + } + + /// Read-only access to [`Memory`]. + #[inline] + pub fn memory(&self, reader: impl FnOnce(&Memory) -> R) -> R { + self.read(move |ctx| reader(&ctx.memory)) + } + + /// Read-write access to [`Memory`]. + #[inline] + pub fn memory_mut(&self, writer: impl FnOnce(&mut Memory) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.memory)) + } + + /// Read-only access to [`IdTypeMap`], which stores superficial widget state. + #[inline] + pub fn data(&self, reader: impl FnOnce(&IdTypeMap) -> R) -> R { + self.read(move |ctx| reader(&ctx.memory.data)) } + /// Read-write access to [`IdTypeMap`], which stores superficial widget state. + #[inline] + pub fn data_mut(&self, writer: impl FnOnce(&mut IdTypeMap) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.memory.data)) + } + + /// Read-write access to [`GraphicLayers`], where painted [`crate::Shape`]s are written to. + #[inline] + pub(crate) fn graphics_mut(&self, writer: impl FnOnce(&mut GraphicLayers) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.graphics)) + } + + /// Read-only access to [`PlatformOutput`]. + /// + /// This is what egui outputs each frame. + /// + /// ``` + /// # let mut ctx = egui::Context::default(); + /// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress); + /// ``` + #[inline] + pub fn output(&self, reader: impl FnOnce(&PlatformOutput) -> R) -> R { + self.read(move |ctx| reader(&ctx.output)) + } + + /// Read-write access to [`PlatformOutput`]. + #[inline] + pub fn output_mut(&self, writer: impl FnOnce(&mut PlatformOutput) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.output)) + } + + /// Read-only access to [`FrameState`]. + #[inline] + pub(crate) fn frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { + self.read(move |ctx| reader(&ctx.frame_state)) + } + + /// Read-write access to [`FrameState`]. + #[inline] + pub(crate) fn frame_state_mut(&self, writer: impl FnOnce(&mut FrameState) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.frame_state)) + } + + /// Read-only access to [`Fonts`]. + /// + /// Not valid until first call to [`Context::run()`]. + /// That's because since we don't know the proper `pixels_per_point` until then. + #[inline] + pub fn fonts(&self, reader: impl FnOnce(&Fonts) -> R) -> R { + self.read(move |ctx| { + reader( + ctx.fonts + .as_ref() + .expect("No fonts available until first call to Context::run()"), + ) + }) + } + + /// Read-write access to [`Fonts`]. + #[inline] + pub fn fonts_mut(&self, writer: impl FnOnce(&mut Option) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.fonts)) + } + + /// Read-only access to [`Options`]. + #[inline] + pub fn options(&self, reader: impl FnOnce(&Options) -> R) -> R { + self.read(move |ctx| reader(&ctx.memory.options)) + } + + /// Read-write access to [`Options`]. + #[inline] + pub fn options_mut(&self, writer: impl FnOnce(&mut Options) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.memory.options)) + } + + /// Read-only access to [`TessellationOptions`]. + #[inline] + pub fn tessellation_options(&self, reader: impl FnOnce(&TessellationOptions) -> R) -> R { + self.read(move |ctx| reader(&ctx.memory.options.tessellation_options)) + } + + /// Read-write access to [`TessellationOptions`]. + #[inline] + pub fn tessellation_options_mut( + &self, + writer: impl FnOnce(&mut TessellationOptions) -> R, + ) -> R { + self.write(move |ctx| writer(&mut ctx.memory.options.tessellation_options)) + } +} + +impl Context { // --------------------------------------------------------------------- /// If the given [`Id`] has been used previously the same frame at at different position, @@ -304,7 +461,7 @@ impl Context { /// The most important thing is that [`Rect::min`] is approximately correct, /// because that's where the warning will be painted. If you don't know what size to pick, just pick [`Vec2::ZERO`]. pub fn check_for_id_clash(&self, id: Id, new_rect: Rect, what: &str) { - let prev_rect = self.frame_state().used_ids.insert(id, new_rect); + let prev_rect = self.frame_state_mut(move |state| state.used_ids.insert(id, new_rect)); if let Some(prev_rect) = prev_rect { // it is ok to reuse the same ID for e.g. a frame around a widget, // or to check for interaction with the same widget twice: @@ -320,7 +477,7 @@ impl Context { let painter = self.debug_painter(); painter.rect_stroke(widget_rect, 0.0, (1.0, color)); - let below = widget_rect.bottom() + 32.0 < self.input().screen_rect.bottom(); + let below = widget_rect.bottom() + 32.0 < self.input(|i| i.screen_rect.bottom()); let text_rect = if below { painter.debug_text( @@ -408,46 +565,45 @@ impl Context { ); } - let mut slf = self.write(); - - slf.layer_rects_this_frame - .entry(layer_id) - .or_default() - .push((id, interact_rect)); - - if hovered { - let pointer_pos = slf.input.pointer.interact_pos(); - if let Some(pointer_pos) = pointer_pos { - if let Some(rects) = slf.layer_rects_prev_frame.get(&layer_id) { - for &(prev_id, prev_rect) in rects.iter().rev() { - if prev_id == id { - break; // there is no other interactive widget covering us at the pointer position. - } - if prev_rect.contains(pointer_pos) { - // Another interactive widget is covering us at the pointer position, - // so we aren't hovered. - - if slf.memory.options.style.debug.show_blocking_widget { - drop(slf); - Self::layer_painter(self, LayerId::debug()).debug_rect( - interact_rect, - Color32::GREEN, - "Covered", - ); - Self::layer_painter(self, LayerId::debug()).debug_rect( - prev_rect, - Color32::LIGHT_BLUE, - "On top", - ); + self.write(|ctx| { + ctx.layer_rects_this_frame + .entry(layer_id) + .or_default() + .push((id, interact_rect)); + + if hovered { + let pointer_pos = ctx.input.pointer.interact_pos(); + if let Some(pointer_pos) = pointer_pos { + if let Some(rects) = ctx.layer_rects_prev_frame.get(&layer_id) { + for &(prev_id, prev_rect) in rects.iter().rev() { + if prev_id == id { + break; // there is no other interactive widget covering us at the pointer position. + } + if prev_rect.contains(pointer_pos) { + // Another interactive widget is covering us at the pointer position, + // so we aren't hovered. + + if ctx.memory.options.style.debug.show_blocking_widget { + Self::layer_painter(self, LayerId::debug()).debug_rect( + interact_rect, + Color32::GREEN, + "Covered", + ); + Self::layer_painter(self, LayerId::debug()).debug_rect( + prev_rect, + Color32::LIGHT_BLUE, + "On top", + ); + } + + hovered = false; + break; } - - hovered = false; - break; } } } } - } + }); } self.interact_with_hovered(layer_id, id, rect, sense, enabled, hovered) @@ -485,7 +641,7 @@ impl Context { if !enabled || !sense.focusable || !layer_id.allow_interaction() { // Not interested or allowed input: - self.memory().surrender_focus(id); + self.memory_mut(|mem| mem.surrender_focus(id)); return response; } @@ -496,116 +652,115 @@ impl Context { // Make sure anything that can receive focus has an AccessKit node. // TODO(mwcampbell): For nodes that are filled from widget info, // some information is written to the node twice. - if let Some(mut node) = self.accesskit_node(id) { - response.fill_accesskit_node_common(&mut node); - } + self.accesskit_node(id, |node| response.fill_accesskit_node_common(node)); } let clicked_elsewhere = response.clicked_elsewhere(); - let ctx_impl = &mut *self.write(); - let memory = &mut ctx_impl.memory; - let input = &mut ctx_impl.input; - - if sense.focusable { - memory.interested_in_focus(id); - } + self.write(|ctx| { + let memory = &mut ctx.memory; + let input = &mut ctx.input; - if sense.click - && memory.has_focus(response.id) - && (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; - } + if sense.focusable { + memory.interested_in_focus(id); + } - #[cfg(feature = "accesskit")] - { if sense.click - && input.has_accesskit_action_request(response.id, accesskit::Action::Default) + && memory.has_focus(response.id) + && (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; } - } - if sense.click || sense.drag { - memory.interaction.click_interest |= hovered && sense.click; - memory.interaction.drag_interest |= hovered && sense.drag; - - response.dragged = memory.interaction.drag_id == Some(id); - response.is_pointer_button_down_on = - memory.interaction.click_id == Some(id) || response.dragged; - - for pointer_event in &input.pointer.pointer_events { - match pointer_event { - PointerEvent::Moved(_) => {} - PointerEvent::Pressed { .. } => { - if hovered { - if sense.click && memory.interaction.click_id.is_none() { - // potential start of a click - memory.interaction.click_id = Some(id); - response.is_pointer_button_down_on = true; - } + #[cfg(feature = "accesskit")] + { + if sense.click + && input.has_accesskit_action_request(response.id, accesskit::Action::Default) + { + response.clicked[PointerButton::Primary as usize] = true; + } + } + + if sense.click || sense.drag { + memory.interaction.click_interest |= hovered && sense.click; + memory.interaction.drag_interest |= hovered && sense.drag; + + response.dragged = memory.interaction.drag_id == Some(id); + response.is_pointer_button_down_on = + memory.interaction.click_id == Some(id) || response.dragged; + + for pointer_event in &input.pointer.pointer_events { + match pointer_event { + PointerEvent::Moved(_) => {} + PointerEvent::Pressed { .. } => { + if hovered { + if sense.click && memory.interaction.click_id.is_none() { + // potential start of a click + memory.interaction.click_id = Some(id); + response.is_pointer_button_down_on = true; + } - // HACK: windows have low priority on dragging. - // This is so that if you drag a slider in a window, - // the slider will steal the drag away from the window. - // This is needed because we do window interaction first (to prevent frame delay), - // and then do content layout. - if sense.drag - && (memory.interaction.drag_id.is_none() - || memory.interaction.drag_is_window) - { - // potential start of a drag - memory.interaction.drag_id = Some(id); - memory.interaction.drag_is_window = false; - memory.window_interaction = None; // HACK: stop moving windows (if any) - response.is_pointer_button_down_on = true; - response.dragged = true; + // HACK: windows have low priority on dragging. + // This is so that if you drag a slider in a window, + // the slider will steal the drag away from the window. + // This is needed because we do window interaction first (to prevent frame delay), + // and then do content layout. + if sense.drag + && (memory.interaction.drag_id.is_none() + || memory.interaction.drag_is_window) + { + // potential start of a drag + memory.interaction.drag_id = Some(id); + memory.interaction.drag_is_window = false; + memory.window_interaction = None; // HACK: stop moving windows (if any) + response.is_pointer_button_down_on = true; + response.dragged = true; + } } } - } - PointerEvent::Released { click, button } => { - response.drag_released = response.dragged; - response.dragged = false; - - if hovered && response.is_pointer_button_down_on { - if let Some(click) = click { - let clicked = hovered && response.is_pointer_button_down_on; - response.clicked[*button as usize] = clicked; - response.double_clicked[*button as usize] = - clicked && click.is_double(); - response.triple_clicked[*button as usize] = - clicked && click.is_triple(); + PointerEvent::Released { click, button } => { + response.drag_released = response.dragged; + response.dragged = false; + + if hovered && response.is_pointer_button_down_on { + if let Some(click) = click { + let clicked = hovered && response.is_pointer_button_down_on; + response.clicked[*button as usize] = clicked; + response.double_clicked[*button as usize] = + clicked && click.is_double(); + response.triple_clicked[*button as usize] = + clicked && click.is_triple(); + } } } } } } - } - if response.is_pointer_button_down_on { - response.interact_pointer_pos = input.pointer.interact_pos(); - } + if response.is_pointer_button_down_on { + response.interact_pointer_pos = input.pointer.interact_pos(); + } - if input.pointer.any_down() { - response.hovered &= response.is_pointer_button_down_on; // we don't hover widgets while interacting with *other* widgets - } + 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) && clicked_elsewhere { - memory.surrender_focus(id); - } + if memory.has_focus(response.id) && clicked_elsewhere { + memory.surrender_focus(id); + } - if response.dragged() && !memory.has_focus(response.id) { - // e.g.: remove focus from a widget when you drag something else - memory.stop_text_input(); - } + if response.dragged() && !memory.has_focus(response.id) { + // e.g.: remove focus from a widget when you drag something else + memory.stop_text_input(); + } + }); response } /// Get a full-screen painter for a new or existing layer pub fn layer_painter(&self, layer_id: LayerId) -> Painter { - let screen_rect = self.input().screen_rect(); + let screen_rect = self.screen_rect(); Painter::new(self.clone(), layer_id, screen_rect) } @@ -614,107 +769,6 @@ impl Context { Self::layer_painter(self, LayerId::debug()) } - /// 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().available_rect() - } -} - -/// ## Borrows parts of [`Context`] -impl Context { - /// Stores all the egui state. - /// - /// If you want to store/restore egui, serialize this. - #[inline] - pub fn memory(&self) -> RwLockWriteGuard<'_, Memory> { - RwLockWriteGuard::map(self.write(), |c| &mut c.memory) - } - - /// Stores superficial widget state. - #[inline] - pub fn data(&self) -> RwLockWriteGuard<'_, crate::util::IdTypeMap> { - RwLockWriteGuard::map(self.write(), |c| &mut c.memory.data) - } - - #[inline] - pub(crate) fn graphics(&self) -> RwLockWriteGuard<'_, GraphicLayers> { - RwLockWriteGuard::map(self.write(), |c| &mut c.graphics) - } - - /// What egui outputs each frame. - /// - /// ``` - /// # let mut ctx = egui::Context::default(); - /// ctx.output().cursor_icon = egui::CursorIcon::Progress; - /// ``` - #[inline] - pub fn output(&self) -> RwLockWriteGuard<'_, PlatformOutput> { - RwLockWriteGuard::map(self.write(), |c| &mut c.output) - } - - #[inline] - 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] - pub fn input(&self) -> RwLockReadGuard<'_, InputState> { - RwLockReadGuard::map(self.read(), |c| &c.input) - } - - #[inline] - pub fn input_mut(&self) -> RwLockWriteGuard<'_, InputState> { - RwLockWriteGuard::map(self.write(), |c| &mut c.input) - } - - /// Not valid until first call to [`Context::run()`]. - /// That's because since we don't know the proper `pixels_per_point` until then. - #[inline] - 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()") - }) - } - - #[inline] - fn fonts_mut(&self) -> RwLockWriteGuard<'_, Option> { - RwLockWriteGuard::map(self.write(), |c| &mut c.fonts) - } - - #[inline] - pub fn options(&self) -> RwLockWriteGuard<'_, Options> { - RwLockWriteGuard::map(self.write(), |c| &mut c.memory.options) - } - - /// Change the options used by the tessellator. - #[inline] - pub fn tessellation_options(&self) -> RwLockWriteGuard<'_, TessellationOptions> { - RwLockWriteGuard::map(self.write(), |c| &mut c.memory.options.tessellation_options) - } - /// What operating system are we running on? /// /// When compiling natively, this is @@ -723,7 +777,7 @@ impl Context { /// For web, this can be figured out from the user-agent, /// and is done so by [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe). pub fn os(&self) -> OperatingSystem { - self.read().os + self.read(|ctx| ctx.os) } /// Set the operating system we are running on. @@ -731,7 +785,18 @@ impl Context { /// If you are writing wasm-based integration for egui you /// may want to set this based on e.g. the user-agent. pub fn set_os(&self, os: OperatingSystem) { - self.write().os = os; + self.write(|ctx| ctx.os = os); + } + + /// Set the cursor icon. + /// + /// Equivalent to: + /// ``` + /// # let ctx = egui::Context::default(); + /// ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::PointingHand); + /// ``` + pub fn set_cursor_icon(&self, cursor_icon: CursorIcon) { + self.output_mut(|o| o.cursor_icon = cursor_icon); } /// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`). @@ -752,13 +817,14 @@ impl Context { } = ModifierNames::SYMBOLS; let font_id = TextStyle::Body.resolve(&self.style()); - let fonts = self.fonts(); - let mut fonts = fonts.lock(); - let font = fonts.fonts.font(&font_id); - font.has_glyphs(alt) - && font.has_glyphs(ctrl) - && font.has_glyphs(shift) - && font.has_glyphs(mac_cmd) + self.fonts(|f| { + let mut lock = f.lock(); + let font = lock.fonts.font(&font_id); + font.has_glyphs(alt) + && font.has_glyphs(ctrl) + && font.has_glyphs(shift) + && font.has_glyphs(mac_cmd) + }) }; if is_mac && can_show_symbols() { @@ -767,9 +833,7 @@ impl Context { shortcut.format(&ModifierNames::NAMES, is_mac) } } -} -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. @@ -780,14 +844,15 @@ impl Context { /// (this will work on `eframe`). pub fn request_repaint(&self) { // request two frames of repaint, just to cover some corner cases (frame delays): - let mut ctx = self.write(); - ctx.repaint_requests = 2; - if let Some(callback) = &ctx.request_repaint_callback { - if !ctx.has_requested_repaint_this_frame { - (callback)(); - ctx.has_requested_repaint_this_frame = true; + self.write(|ctx| { + ctx.repaint_requests = 2; + if let Some(callback) = &ctx.request_repaint_callback { + if !ctx.has_requested_repaint_this_frame { + (callback)(); + ctx.has_requested_repaint_this_frame = true; + } } - } + }); } /// Request repaint after the specified duration elapses in the case of no new input @@ -819,8 +884,7 @@ impl Context { /// during app idle time where we are not receiving any new input events. pub fn request_repaint_after(&self, duration: std::time::Duration) { // Maybe we can check if duration is ZERO, and call self.request_repaint()? - let mut ctx = self.write(); - ctx.repaint_after = ctx.repaint_after.min(duration); + self.write(|ctx| ctx.repaint_after = ctx.repaint_after.min(duration)); } /// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`]. @@ -830,7 +894,7 @@ impl Context { /// Note that only one callback can be set. Any new call overrides the previous callback. pub fn set_request_repaint_callback(&self, callback: impl Fn() + Send + Sync + 'static) { let callback = Box::new(callback); - self.write().request_repaint_callback = Some(callback); + self.write(|ctx| ctx.request_repaint_callback = Some(callback)); } /// Tell `egui` which fonts to use. @@ -840,19 +904,23 @@ 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_mut() { - // NOTE: this comparison is expensive since it checks TTF data for equality - if current_fonts.lock().fonts.definitions() == &font_definitions { - return; // no change - save us from reloading font textures + let update_fonts = self.fonts_mut(|fonts| { + if let Some(current_fonts) = fonts { + // NOTE: this comparison is expensive since it checks TTF data for equality + current_fonts.lock().fonts.definitions() != &font_definitions + } else { + true } - } + }); - self.memory().new_font_definitions = Some(font_definitions); + if update_fonts { + self.memory_mut(|mem| mem.new_font_definitions = Some(font_definitions)); + } } /// The [`Style`] used by all subsequent windows, panels etc. pub fn style(&self) -> Arc