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:
parent
226bdc4c5b
commit
f2f00ef62a
|
|
@ -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()
|
||||
));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue