New Plugin trait (#7385)

This adds a new `Plugin` trait and new `input_hook` and `output_hook`
plugin fns. Having a `Plugin` trait should make it easier to store state
in the plugin and improve discoverability of possible plugin hooks.

The old `on_begin_pass` and `on_end_pass` have been ported to use the
new plugin trait, should we deprecate them?
This commit is contained in:
Lucas Meurer 2025-09-16 10:55:58 +02:00 committed by GitHub
parent 226bdc4c5b
commit f2f00ef62a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 404 additions and 191 deletions

View File

@ -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<dyn Fn(&Context) + Send + Sync>;
#[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<NamedContextCallback>,
pub on_end_pass: Vec<NamedContextCallback>,
}
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<T: Plugin + 'static, R>(&self, f: impl FnOnce(&mut T) -> R) -> Option<R> {
let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::<T>()));
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<T: Plugin>(&self) -> TypedPluginHandle<T> {
if let Some(plugin) = self.plugin_opt() {
plugin
} else {
panic!("Plugin of type {:?} not found", std::any::type_name::<T>());
}
}
/// Get a handle to the plugin of type `T`, if it was registered.
pub fn plugin_opt<T: Plugin>(&self) -> Option<TypedPluginHandle<T>> {
let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::<T>()));
plugin.map(TypedPluginHandle::new)
}
/// Get a handle to the plugin of type `T`, or insert its default.
pub fn plugin_or_default<T: Plugin + Default>(&self) -> TypedPluginHandle<T> {
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::<crate::text_selection::LabelSelectionState>()
.lock()
));
});

View File

@ -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<WidgetText>) {
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::<State>(Id::NULL);
state.entries.push(Entry {
location,
text: text.into(),
});
let plugin = ctx.plugin::<DebugTextPlugin>();
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<Entry>,
}
impl State {
fn end_pass(ctx: &Context) {
let state = ctx.data_mut(|data| data.remove_temp::<Self>(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<Entry>) {
if entries.is_empty() {
return;
}

View File

@ -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<Arc<dyn Any + Send + Sync>>,
}
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::<Self>(Id::NULL);
state.payload = Some(Arc::new(payload));
});
ctx.plugin::<Self>().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::<Self>(Id::NULL);
state.payload = None;
});
ctx.plugin::<Self>().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::<Self>(Id::NULL)?;
let payload = state.payload?;
payload.downcast().ok()
})
ctx.plugin::<Self>()
.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::<Self>(Id::NULL);
let payload = state.payload.take()?;
payload.downcast().ok()
})
ctx.plugin::<Self>().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::<Self>(Id::NULL);
state.is_some_and(|state| state.payload.is_some())
})
ctx.plugin::<Self>().lock().payload.is_some()
}
}

View File

@ -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},

232
crates/egui/src/plugin.rs Normal file
View File

@ -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<dyn Plugin>,
}
pub struct TypedPluginHandle<P: Plugin> {
handle: Arc<Mutex<PluginHandle>>,
_type: std::marker::PhantomData<P>,
}
impl<P: Plugin> TypedPluginHandle<P> {
pub(crate) fn new(handle: Arc<Mutex<PluginHandle>>) -> 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<P>,
}
impl<P: Plugin> TypedPluginGuard<'_, P> {}
impl<P: Plugin> std::ops::Deref for TypedPluginGuard<'_, P> {
type Target = P;
fn deref(&self) -> &Self::Target {
self.guard.typed_plugin()
}
}
impl<P: Plugin> std::ops::DerefMut for TypedPluginGuard<'_, P> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.guard.typed_plugin_mut()
}
}
impl PluginHandle {
pub fn new<P: Plugin>(plugin: P) -> Arc<Mutex<Self>> {
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<P: Plugin + 'static>(&self) -> &P {
(&*self.plugin as &dyn std::any::Any)
.downcast_ref::<P>()
.expect("PluginHandle: plugin is not of the expected type")
}
pub fn typed_plugin_mut<P: Plugin + 'static>(&mut self) -> &mut P {
(&mut *self.plugin as &mut dyn std::any::Any)
.downcast_mut::<P>()
.expect("PluginHandle: plugin is not of the expected type")
}
}
/// User-registered plugins.
#[derive(Clone, Default)]
pub(crate) struct Plugins {
plugins: HashMap<std::any::TypeId, Arc<Mutex<PluginHandle>>>,
plugins_ordered: PluginsOrdered,
}
#[derive(Clone, Default)]
pub(crate) struct PluginsOrdered(Vec<Arc<Mutex<PluginHandle>>>);
impl PluginsOrdered {
fn for_each_dyn<F>(&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<Mutex<PluginHandle>>) -> 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<Arc<Mutex<PluginHandle>>> {
self.plugins.get(&type_id).cloned()
}
}
/// Generic event callback.
pub type ContextCallback = Arc<dyn Fn(&Context) + Send + Sync>;
#[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);
}
}
}

View File

@ -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::<Self>(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::<Self>();
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(