diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index cdc0199e..4ac8668d 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -17,31 +17,32 @@ use epaint::{ use crate::{ Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport, ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, - ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText, + ModifierNames, Modifiers, NumExt as _, Order, Painter, Plugin, RawInput, Response, RichText, ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, Widget as _, WidgetRect, WidgetText, animation_manager::AnimationManager, containers::{self, area::AreaState}, data::output::PlatformOutput, - epaint, hit_test, - input_state::{InputState, MultiTouchInfo, PointerEvent}, - interaction, + epaint, + hit_test::WidgetHits, + input_state::{InputState, MultiTouchInfo, PointerEvent, SurrenderFocusOn}, + interaction::InteractionSnapshot, layers::GraphicLayers, load::{self, Bytes, Loaders, SizedTexture}, memory::{Options, Theme}, os::OperatingSystem, output::FullOutput, pass_state::PassState, + plugin, + plugin::TypedPluginHandle, resize, response, scroll_area, util::IdTypeMap, viewport::ViewportClass, }; -use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; #[cfg(feature = "accesskit")] use crate::IdMap; -use crate::input_state::SurrenderFocusOn; /// Information given to the backend about when it is time to repaint the ui. /// @@ -93,46 +94,6 @@ impl Default for WrappedTextureManager { // ---------------------------------------------------------------------------- -/// Generic event callback. -pub type ContextCallback = Arc; - -#[derive(Clone)] -struct NamedContextCallback { - debug_name: &'static str, - callback: ContextCallback, -} - -/// Callbacks that users can register -#[derive(Clone, Default)] -struct Plugins { - pub on_begin_pass: Vec, - pub on_end_pass: Vec, -} - -impl Plugins { - fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) { - profiling::scope!("plugins", _cb_name); - for NamedContextCallback { - debug_name: _name, - callback, - } in callbacks - { - profiling::scope!("plugin", _name); - (callback)(ctx); - } - } - - fn on_begin_pass(&self, ctx: &Context) { - Self::call(ctx, "on_begin_pass", &self.on_begin_pass); - } - - fn on_end_pass(&self, ctx: &Context) { - Self::call(ctx, "on_end_pass", &self.on_end_pass); - } -} - -// ---------------------------------------------------------------------------- - /// Repaint-logic impl ContextImpl { /// This is where we update the repaint logic. @@ -412,7 +373,7 @@ struct ContextImpl { memory: Memory, animation_manager: AnimationManager, - plugins: Plugins, + plugins: plugin::Plugins, /// All viewports share the same texture manager and texture namespace. /// @@ -756,10 +717,12 @@ impl Default for Context { }; let ctx = Self(Arc::new(RwLock::new(ctx_impl))); + ctx.add_plugin(plugin::CallbackPlugin::default()); + // Register built-in plugins: - crate::debug_text::register(&ctx); - crate::text_selection::LabelSelectionState::register(&ctx); - crate::DragAndDrop::register(&ctx); + ctx.add_plugin(crate::debug_text::DebugTextPlugin::default()); + ctx.add_plugin(crate::text_selection::LabelSelectionState::default()); + ctx.add_plugin(crate::DragAndDrop::default()); ctx } @@ -889,13 +852,16 @@ impl Context { /// let full_output = ctx.end_pass(); /// // handle full_output /// ``` - pub fn begin_pass(&self, new_input: RawInput) { + pub fn begin_pass(&self, mut new_input: RawInput) { profiling::function_scope!(); + let plugins = self.read(|ctx| ctx.plugins.ordered_plugins()); + plugins.on_input(&mut new_input); + self.write(|ctx| ctx.begin_pass(new_input)); // Plugins run just after the pass starts: - self.read(|ctx| ctx.plugins.clone()).on_begin_pass(self); + plugins.on_begin_pass(self); } /// See [`Self::begin_pass`]. @@ -1885,26 +1851,73 @@ impl Context { impl Context { /// Call the given callback at the start of each pass of each viewport. /// - /// This can be used for egui _plugins_. - /// See [`crate::debug_text`] for an example. - pub fn on_begin_pass(&self, debug_name: &'static str, cb: ContextCallback) { - let named_cb = NamedContextCallback { - debug_name, - callback: cb, - }; - self.write(|ctx| ctx.plugins.on_begin_pass.push(named_cb)); + /// This is a convenience wrapper around [`Self::add_plugin`]. + pub fn on_begin_pass(&self, debug_name: &'static str, cb: plugin::ContextCallback) { + self.with_plugin(|p: &mut crate::plugin::CallbackPlugin| { + p.on_begin_plugins.push((debug_name, cb)); + }); } /// Call the given callback at the end of each pass of each viewport. /// - /// This can be used for egui _plugins_. - /// See [`crate::debug_text`] for an example. - pub fn on_end_pass(&self, debug_name: &'static str, cb: ContextCallback) { - let named_cb = NamedContextCallback { - debug_name, - callback: cb, - }; - self.write(|ctx| ctx.plugins.on_end_pass.push(named_cb)); + /// This is a convenience wrapper around [`Self::add_plugin`]. + pub fn on_end_pass(&self, debug_name: &'static str, cb: plugin::ContextCallback) { + self.with_plugin(|p: &mut crate::plugin::CallbackPlugin| { + p.on_end_plugins.push((debug_name, cb)); + }); + } + + /// Register a [`Plugin`] + /// + /// Plugins are called in the order they are added. + /// + /// A plugin of the same type can only be added once (further calls with the same type will be ignored). + /// This way it's convenient to add plugins in `eframe::run_simple_native`. + pub fn add_plugin(&self, plugin: impl Plugin + 'static) { + let handle = plugin::PluginHandle::new(plugin); + + let added = self.write(|ctx| ctx.plugins.add(handle.clone())); + + if added { + handle.lock().dyn_plugin_mut().setup(self); + } + } + + /// Call the provided closure with the plugin of type `T`, if it was registered. + /// + /// Returns `None` if the plugin was not registered. + pub fn with_plugin(&self, f: impl FnOnce(&mut T) -> R) -> Option { + let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::())); + plugin.map(|plugin| f(plugin.lock().typed_plugin_mut())) + } + + /// Get a handle to the plugin of type `T`. + /// + /// ## Panics + /// If the plugin of type `T` was not registered, this will panic. + pub fn plugin(&self) -> TypedPluginHandle { + if let Some(plugin) = self.plugin_opt() { + plugin + } else { + panic!("Plugin of type {:?} not found", std::any::type_name::()); + } + } + + /// Get a handle to the plugin of type `T`, if it was registered. + pub fn plugin_opt(&self) -> Option> { + let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::())); + plugin.map(TypedPluginHandle::new) + } + + /// Get a handle to the plugin of type `T`, or insert its default. + pub fn plugin_or_default(&self) -> TypedPluginHandle { + if let Some(plugin) = self.plugin_opt() { + plugin + } else { + let default_plugin = T::default(); + self.add_plugin(default_plugin); + self.plugin() + } } } @@ -2265,12 +2278,15 @@ impl Context { } // Plugins run just before the pass ends. - self.read(|ctx| ctx.plugins.clone()).on_end_pass(self); + let plugins = self.read(|ctx| ctx.plugins.ordered_plugins()); + plugins.on_end_pass(self); #[cfg(debug_assertions)] self.debug_painting(); - self.write(|ctx| ctx.end_pass()) + let mut output = self.write(|ctx| ctx.end_pass()); + plugins.on_output(&mut output); + output } /// Call at the end of each frame if you called [`Context::begin_pass`]. @@ -3170,7 +3186,9 @@ impl Context { .show(ui, |ui| { ui.label(format!( "{:#?}", - crate::text_selection::LabelSelectionState::load(ui.ctx()) + *ui.ctx() + .plugin::() + .lock() )); }); diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index c3f93871..ed0bea72 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -1,24 +1,14 @@ //! This is an example of how to create a plugin for egui. //! -//! A plugin usually consist of a struct that holds some state, -//! which is stored using [`Context::data_mut`]. -//! The plugin registers itself onto a specific [`Context`] -//! to get callbacks on certain events ([`Context::on_begin_pass`], [`Context::on_end_pass`]). +//! A plugin is a struct that implements the [`Plugin`] trait and holds some state. +//! The plugin is registered with the [`Context`] using [`Context::add_plugin`] +//! to get callbacks on certain events ([`Plugin::on_begin_pass`], [`Plugin::on_end_pass`]). use crate::{ - Align, Align2, Color32, Context, FontFamily, FontId, Id, Rect, Shape, Vec2, WidgetText, text, + Align, Align2, Color32, Context, FontFamily, FontId, Plugin, Rect, Shape, Vec2, WidgetText, + text, }; -/// Register this plugin on the given egui context, -/// so that it will be called every pass. -/// -/// This is a built-in plugin in egui, -/// meaning [`Context`] calls this from its `Default` implementation, -/// so this is marked as `pub(crate)`. -pub(crate) fn register(ctx: &Context) { - ctx.on_end_pass("debug_text", std::sync::Arc::new(State::end_pass)); -} - /// Print this text next to the cursor at the end of the pass. /// /// If you call this multiple times, the text will be appended. @@ -38,15 +28,12 @@ pub fn print(ctx: &Context, text: impl Into) { let location = std::panic::Location::caller(); let location = format!("{}:{}", location.file(), location.line()); - ctx.data_mut(|data| { - // We use `Id::NULL` as the id, since we only have one instance of this plugin. - // We use the `temp` version instead of `persisted` since we don't want to - // persist state on disk when the egui app is closed. - let state = data.get_temp_mut_or_default::(Id::NULL); - state.entries.push(Entry { - location, - text: text.into(), - }); + + let plugin = ctx.plugin::(); + let mut state = plugin.lock(); + state.entries.push(Entry { + location, + text: text.into(), }); } @@ -58,24 +45,26 @@ struct Entry { /// A plugin for easily showing debug-text on-screen. /// -/// This is a built-in plugin in egui. +/// This is a built-in plugin in egui, automatically registered during [`Context`] creation. #[derive(Clone, Default)] -struct State { +pub struct DebugTextPlugin { // This gets re-filled every pass. entries: Vec, } -impl State { - fn end_pass(ctx: &Context) { - let state = ctx.data_mut(|data| data.remove_temp::(Id::NULL)); - if let Some(state) = state { - state.paint(ctx); - } +impl Plugin for DebugTextPlugin { + fn debug_name(&self) -> &'static str { + "DebugTextPlugin" } - fn paint(self, ctx: &Context) { - let Self { entries } = self; + fn on_end_pass(&mut self, ctx: &Context) { + let entries = std::mem::take(&mut self.entries); + Self::paint_entries(ctx, entries); + } +} +impl DebugTextPlugin { + fn paint_entries(ctx: &Context, entries: Vec) { if entries.is_empty() { return; } diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index 6caecc24..21468ed4 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -1,44 +1,46 @@ use std::{any::Any, sync::Arc}; -use crate::{Context, CursorIcon, Id}; +use crate::{Context, CursorIcon, Plugin}; -/// Tracking of drag-and-drop payload. +/// Plugin for tracking drag-and-drop payload. /// -/// This is a low-level API. +/// This plugin stores the current drag-and-drop payload internally and handles +/// automatic cleanup when the drag operation ends (via Escape key or mouse release). /// -/// For a higher-level API, see: +/// This is a low-level API. For a higher-level API, see: /// - [`crate::Ui::dnd_drag_source`] /// - [`crate::Ui::dnd_drop_zone`] /// - [`crate::Response::dnd_set_drag_payload`] /// - [`crate::Response::dnd_hover_payload`] /// - [`crate::Response::dnd_release_payload`] /// +/// This is a built-in plugin in egui, automatically registered during [`Context`] creation. +/// /// See [this example](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/drag_and_drop.rs). #[doc(alias = "drag and drop")] #[derive(Clone, Default)] pub struct DragAndDrop { - /// If set, something is currently being dragged + /// The current drag-and-drop payload, if any. Automatically cleared when drag ends. payload: Option>, } -impl DragAndDrop { - pub(crate) fn register(ctx: &Context) { - ctx.on_begin_pass("drag_and_drop_begin_pass", Arc::new(Self::begin_pass)); - ctx.on_end_pass("drag_and_drop_end_pass", Arc::new(Self::end_pass)); +impl Plugin for DragAndDrop { + fn debug_name(&self) -> &'static str { + "DragAndDrop" } /// Interrupt drag-and-drop if the user presses the escape key. /// /// This needs to happen at frame start so we can properly capture the escape key. - fn begin_pass(ctx: &Context) { - let has_any_payload = Self::has_any_payload(ctx); + fn on_begin_pass(&mut self, ctx: &Context) { + let has_any_payload = self.payload.is_some(); if has_any_payload { let abort_dnd_due_to_escape_key = ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape)); if abort_dnd_due_to_escape_key { - Self::clear_payload(ctx); + self.payload = None; } } } @@ -48,14 +50,14 @@ impl DragAndDrop { /// This is a catch-all safety net in case user code doesn't capture the drag payload itself. /// This must happen at end-of-frame such that we don't shadow the mouse release event from user /// code. - fn end_pass(ctx: &Context) { - let has_any_payload = Self::has_any_payload(ctx); + fn on_end_pass(&mut self, ctx: &Context) { + let has_any_payload = self.payload.is_some(); if has_any_payload { let abort_dnd_due_to_mouse_release = ctx.input_mut(|i| i.pointer.any_released()); if abort_dnd_due_to_mouse_release { - Self::clear_payload(ctx); + self.payload = None; } else { // We set the cursor icon only if its default, as the user code might have // explicitly set it already. @@ -67,7 +69,9 @@ impl DragAndDrop { } } } +} +impl DragAndDrop { /// Set a drag-and-drop payload. /// /// This can be read by [`Self::payload`] until the pointer is released. @@ -75,18 +79,12 @@ impl DragAndDrop { where Payload: Any + Send + Sync, { - ctx.data_mut(|data| { - let state = data.get_temp_mut_or_default::(Id::NULL); - state.payload = Some(Arc::new(payload)); - }); + ctx.plugin::().lock().payload = Some(Arc::new(payload)); } /// Clears the payload, setting it to `None`. pub fn clear_payload(ctx: &Context) { - ctx.data_mut(|data| { - let state = data.get_temp_mut_or_default::(Id::NULL); - state.payload = None; - }); + ctx.plugin::().lock().payload = None; } /// Retrieve the payload, if any. @@ -99,11 +97,13 @@ impl DragAndDrop { where Payload: Any + Send + Sync, { - ctx.data(|data| { - let state = data.get_temp::(Id::NULL)?; - let payload = state.payload?; - payload.downcast().ok() - }) + ctx.plugin::() + .lock() + .payload + .as_ref()? + .clone() + .downcast() + .ok() } /// Retrieve and clear the payload, if any. @@ -116,11 +116,7 @@ impl DragAndDrop { where Payload: Any + Send + Sync, { - ctx.data_mut(|data| { - let state = data.get_temp_mut_or_default::(Id::NULL); - let payload = state.payload.take()?; - payload.downcast().ok() - }) + ctx.plugin::().lock().payload.take()?.downcast().ok() } /// Are we carrying a payload of the given type? @@ -139,9 +135,6 @@ impl DragAndDrop { /// Returns `true` both during a drag and on the frame the pointer is released /// (if there is a payload). pub fn has_any_payload(ctx: &Context) -> bool { - ctx.data(|data| { - let state = data.get_temp::(Id::NULL); - state.is_some_and(|state| state.payload.is_some()) - }) + ctx.plugin::().lock().payload.is_some() } } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index f8e9308a..8b87866d 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -406,6 +406,7 @@ #![allow(clippy::manual_range_contains)] mod animation_manager; +mod atomics; pub mod cache; pub mod containers; mod context; @@ -429,6 +430,7 @@ pub mod os; mod painter; mod pass_state; pub(crate) mod placer; +mod plugin; pub mod response; mod sense; pub mod style; @@ -442,7 +444,6 @@ mod widget_rect; pub mod widget_text; pub mod widgets; -mod atomics; #[cfg(feature = "callstack")] #[cfg(debug_assertions)] mod callstack; @@ -501,6 +502,7 @@ pub use self::{ load::SizeHint, memory::{FocusDirection, Memory, Options, Theme, ThemePreference}, painter::Painter, + plugin::Plugin, response::{InnerResponse, Response}, sense::Sense, style::{FontSelection, Spacing, Style, TextStyle, Visuals}, diff --git a/crates/egui/src/plugin.rs b/crates/egui/src/plugin.rs new file mode 100644 index 00000000..bebcf892 --- /dev/null +++ b/crates/egui/src/plugin.rs @@ -0,0 +1,232 @@ +use crate::{Context, FullOutput, RawInput}; +use ahash::HashMap; +use epaint::mutex::{Mutex, MutexGuard}; +use std::sync::Arc; + +/// A plugin to extend egui. +/// +/// Add plugins via [`Context::add_plugin`]. +/// +/// Plugins should not hold a reference to the [`Context`], since this would create a cycle +/// (which would prevent the [`Context`] from being dropped). +#[expect(unused_variables)] +pub trait Plugin: Send + Sync + std::any::Any + 'static { + /// Plugin name. + /// + /// Used when profiling. + fn debug_name(&self) -> &'static str; + + /// Called once, when the plugin is registered. + /// + /// Useful to e.g. register image loaders. + fn setup(&mut self, ctx: &Context) {} + + /// Called at the start of each pass. + /// + /// Can be used to show ui, e.g. a [`crate::Window`] or [`crate::SidePanel`]. + fn on_begin_pass(&mut self, ctx: &Context) {} + + /// Called at the end of each pass. + /// + /// Can be used to show ui, e.g. a [`crate::Window`]. + fn on_end_pass(&mut self, ctx: &Context) {} + + /// Called just before the input is processed. + /// + /// Useful to inspect or modify the input. + /// Since this is called outside a pass, don't show ui here. + fn input_hook(&mut self, input: &mut RawInput) {} + + /// Called just before the output is passed to the backend. + /// + /// Useful to inspect or modify the output. + /// Since this is called outside a pass, don't show ui here. + fn output_hook(&mut self, output: &mut FullOutput) {} +} + +pub(crate) struct PluginHandle { + plugin: Box, +} + +pub struct TypedPluginHandle { + handle: Arc>, + _type: std::marker::PhantomData

, +} + +impl TypedPluginHandle

{ + pub(crate) fn new(handle: Arc>) -> Self { + Self { + handle, + _type: std::marker::PhantomData, + } + } + + pub fn lock(&self) -> TypedPluginGuard<'_, P> { + TypedPluginGuard { + guard: self.handle.lock(), + _type: std::marker::PhantomData, + } + } +} + +pub struct TypedPluginGuard<'a, P: Plugin> { + guard: MutexGuard<'a, PluginHandle>, + _type: std::marker::PhantomData

, +} + +impl TypedPluginGuard<'_, P> {} + +impl std::ops::Deref for TypedPluginGuard<'_, P> { + type Target = P; + + fn deref(&self) -> &Self::Target { + self.guard.typed_plugin() + } +} + +impl std::ops::DerefMut for TypedPluginGuard<'_, P> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.guard.typed_plugin_mut() + } +} + +impl PluginHandle { + pub fn new(plugin: P) -> Arc> { + Arc::new(Mutex::new(Self { + plugin: Box::new(plugin), + })) + } + + fn plugin_type_id(&self) -> std::any::TypeId { + (*self.plugin).type_id() + } + + pub fn dyn_plugin_mut(&mut self) -> &mut dyn Plugin { + &mut *self.plugin + } + + fn typed_plugin(&self) -> &P { + (&*self.plugin as &dyn std::any::Any) + .downcast_ref::

() + .expect("PluginHandle: plugin is not of the expected type") + } + + pub fn typed_plugin_mut(&mut self) -> &mut P { + (&mut *self.plugin as &mut dyn std::any::Any) + .downcast_mut::

() + .expect("PluginHandle: plugin is not of the expected type") + } +} + +/// User-registered plugins. +#[derive(Clone, Default)] +pub(crate) struct Plugins { + plugins: HashMap>>, + plugins_ordered: PluginsOrdered, +} + +#[derive(Clone, Default)] +pub(crate) struct PluginsOrdered(Vec>>); + +impl PluginsOrdered { + fn for_each_dyn(&self, mut f: F) + where + F: FnMut(&mut dyn Plugin), + { + for plugin in &self.0 { + let mut plugin = plugin.lock(); + profiling::scope!("plugin", plugin.dyn_plugin_mut().debug_name()); + f(plugin.dyn_plugin_mut()); + } + } + + pub fn on_begin_pass(&self, ctx: &Context) { + profiling::scope!("plugins", "on_begin_pass"); + self.for_each_dyn(|p| { + p.on_begin_pass(ctx); + }); + } + + pub fn on_end_pass(&self, ctx: &Context) { + profiling::scope!("plugins", "on_end_pass"); + self.for_each_dyn(|p| { + p.on_end_pass(ctx); + }); + } + + pub fn on_input(&self, input: &mut RawInput) { + profiling::scope!("plugins", "on_input"); + self.for_each_dyn(|plugin| { + plugin.input_hook(input); + }); + } + + pub fn on_output(&self, output: &mut FullOutput) { + profiling::scope!("plugins", "on_output"); + self.for_each_dyn(|plugin| { + plugin.output_hook(output); + }); + } +} + +impl Plugins { + pub fn ordered_plugins(&self) -> PluginsOrdered { + self.plugins_ordered.clone() + } + + /// Remember to call [`Plugin::setup`] on the plugin after adding it. + /// + /// Will not add the plugin if a plugin of the same type already exists. + /// Returns `false` if the plugin was not added, `true` if it was added. + pub fn add(&mut self, handle: Arc>) -> bool { + profiling::scope!("plugins", "add"); + + let type_id = handle.lock().plugin_type_id(); + + if self.plugins.contains_key(&type_id) { + return false; + } + + self.plugins.insert(type_id, handle.clone()); + self.plugins_ordered.0.push(handle); + + true + } + + pub fn get(&self, type_id: std::any::TypeId) -> Option>> { + self.plugins.get(&type_id).cloned() + } +} + +/// Generic event callback. +pub type ContextCallback = Arc; + +#[derive(Default)] +pub(crate) struct CallbackPlugin { + pub on_begin_plugins: Vec<(&'static str, ContextCallback)>, + pub on_end_plugins: Vec<(&'static str, ContextCallback)>, +} + +impl Plugin for CallbackPlugin { + fn debug_name(&self) -> &'static str { + "CallbackPlugins" + } + + fn on_begin_pass(&mut self, ctx: &Context) { + profiling::function_scope!(); + + for (_debug_name, cb) in &self.on_begin_plugins { + profiling::scope!(*_debug_name); + (cb)(ctx); + } + } + + fn on_end_pass(&mut self, ctx: &Context) { + profiling::function_scope!(); + + for (_debug_name, cb) in &self.on_end_plugins { + profiling::scope!(*_debug_name); + (cb)(ctx); + } + } +} diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 8297bc42..da248a0f 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use emath::TSTransform; use crate::{ - Context, CursorIcon, Event, Galley, Id, LayerId, Pos2, Rect, Response, Ui, layers::ShapeIdx, - text::CCursor, text_selection::CCursorRange, + Context, CursorIcon, Event, Galley, Id, LayerId, Plugin, Pos2, Rect, Response, Ui, + layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, }; use super::{ @@ -123,65 +123,45 @@ impl Default for LabelSelectionState { } } -impl LabelSelectionState { - pub(crate) fn register(ctx: &Context) { - ctx.on_begin_pass("LabelSelectionState", std::sync::Arc::new(Self::begin_pass)); - ctx.on_end_pass("LabelSelectionState", std::sync::Arc::new(Self::end_pass)); +impl Plugin for LabelSelectionState { + fn debug_name(&self) -> &'static str { + "LabelSelectionState" } - pub fn load(ctx: &Context) -> Self { - let id = Id::new(ctx.viewport_id()); - ctx.data(|data| data.get_temp::(id)) - .unwrap_or_default() - } - - pub fn store(self, ctx: &Context) { - let id = Id::new(ctx.viewport_id()); - ctx.data_mut(|data| { - data.insert_temp(id, self); - }); - } - - fn begin_pass(ctx: &Context) { - let mut state = Self::load(ctx); - + fn on_begin_pass(&mut self, ctx: &Context) { if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) { // Maybe a new selection is about to begin, but the old one is over: // state.selection = None; // TODO(emilk): this makes sense, but doesn't work as expected. } - state.selection_bbox_last_frame = state.selection_bbox_this_frame; - state.selection_bbox_this_frame = Rect::NOTHING; + self.selection_bbox_last_frame = self.selection_bbox_this_frame; + self.selection_bbox_this_frame = Rect::NOTHING; - state.any_hovered = false; - state.has_reached_primary = false; - state.has_reached_secondary = false; - state.text_to_copy.clear(); - state.last_copied_galley_rect = None; - state.painted_selections.clear(); - - state.store(ctx); + self.any_hovered = false; + self.has_reached_primary = false; + self.has_reached_secondary = false; + self.text_to_copy.clear(); + self.last_copied_galley_rect = None; + self.painted_selections.clear(); } - fn end_pass(ctx: &Context) { - let mut state = Self::load(ctx); - - if state.is_dragging { + fn on_end_pass(&mut self, ctx: &Context) { + if self.is_dragging { ctx.set_cursor_icon(CursorIcon::Text); } - if !state.has_reached_primary || !state.has_reached_secondary { + if !self.has_reached_primary || !self.has_reached_secondary { // We didn't see both cursors this frame, // maybe because they are outside the visible area (scrolling), // or one disappeared. In either case we will have horrible glitches, so let's just deselect. - let prev_selection = state.selection.take(); + let prev_selection = self.selection.take(); if let Some(selection) = prev_selection { // This was the first frame of glitch, so hide the // glitching by removing all painted selections: ctx.graphics_mut(|layers| { if let Some(list) = layers.get_mut(selection.layer_id) { - for (shape_idx, row_selections) in state.painted_selections.drain(..) { + for (shape_idx, row_selections) in self.painted_selections.drain(..) { list.mutate_shape(shape_idx, |shape| { if let epaint::Shape::Text(text_shape) = &mut shape.shape { let galley = Arc::make_mut(&mut text_shape.galley); @@ -211,25 +191,25 @@ impl LabelSelectionState { } let pressed_escape = ctx.input(|i| i.key_pressed(crate::Key::Escape)); - let clicked_something_else = ctx.input(|i| i.pointer.any_pressed()) && !state.any_hovered; + let clicked_something_else = ctx.input(|i| i.pointer.any_pressed()) && !self.any_hovered; let delected_everything = pressed_escape || clicked_something_else; if delected_everything { - state.selection = None; + self.selection = None; } if ctx.input(|i| i.pointer.any_released()) { - state.is_dragging = false; + self.is_dragging = false; } - let text_to_copy = std::mem::take(&mut state.text_to_copy); + let text_to_copy = std::mem::take(&mut self.text_to_copy); if !text_to_copy.is_empty() { ctx.copy_text(text_to_copy); } - - state.store(ctx); } +} +impl LabelSelectionState { pub fn has_selection(&self) -> bool { self.selection.is_some() } @@ -297,7 +277,8 @@ impl LabelSelectionState { fallback_color: epaint::Color32, underline: epaint::Stroke, ) { - let mut state = Self::load(ui.ctx()); + let plugin = ui.ctx().plugin::(); + let mut state = plugin.lock(); let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley); let shape_idx = ui.painter().add( @@ -309,8 +290,6 @@ impl LabelSelectionState { .painted_selections .push((shape_idx, new_vertex_indices)); } - - state.store(ui.ctx()); } fn cursor_for(