mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 21:30:03 -04:00
Merge branch 'emilk:master' into cache_galley_lines
This commit is contained in:
69
crates/egui/src/cache/cache_storage.rs
vendored
Normal file
69
crates/egui/src/cache/cache_storage.rs
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
use super::CacheTrait;
|
||||
|
||||
/// A typemap of many caches, all implemented with [`CacheTrait`].
|
||||
///
|
||||
/// You can access egui's caches via [`crate::Memory::caches`],
|
||||
/// found with [`crate::Context::memory_mut`].
|
||||
///
|
||||
/// ```
|
||||
/// use egui::cache::{CacheStorage, ComputerMut, FrameCache};
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct CharCounter {}
|
||||
/// impl ComputerMut<&str, usize> for CharCounter {
|
||||
/// fn compute(&mut self, s: &str) -> usize {
|
||||
/// s.chars().count()
|
||||
/// }
|
||||
/// }
|
||||
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
|
||||
///
|
||||
/// # let mut cache_storage = CacheStorage::default();
|
||||
/// let mut cache = cache_storage.cache::<CharCountCache<'_>>();
|
||||
/// assert_eq!(cache.get("hello"), 5);
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
pub struct CacheStorage {
|
||||
caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
|
||||
}
|
||||
|
||||
impl CacheStorage {
|
||||
pub fn cache<Cache: CacheTrait + Default>(&mut self) -> &mut Cache {
|
||||
self.caches
|
||||
.entry(std::any::TypeId::of::<Cache>())
|
||||
.or_insert_with(|| Box::<Cache>::default())
|
||||
.as_any_mut()
|
||||
.downcast_mut::<Cache>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Total number of cached values
|
||||
fn num_values(&self) -> usize {
|
||||
self.caches.values().map(|cache| cache.len()).sum()
|
||||
}
|
||||
|
||||
/// Call once per frame to evict cache.
|
||||
pub fn update(&mut self) {
|
||||
self.caches.retain(|_, cache| {
|
||||
cache.update();
|
||||
cache.len() > 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for CacheStorage {
|
||||
fn clone(&self) -> Self {
|
||||
// We return an empty cache that can be filled in again.
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CacheStorage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"FrameCacheStorage[{} caches with {} elements]",
|
||||
self.caches.len(),
|
||||
self.num_values()
|
||||
)
|
||||
}
|
||||
}
|
||||
11
crates/egui/src/cache/cache_trait.rs
vendored
Normal file
11
crates/egui/src/cache/cache_trait.rs
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/// A cache, storing some value for some length of time.
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub trait CacheTrait: 'static + Send + Sync {
|
||||
/// Call once per frame to evict cache.
|
||||
fn update(&mut self);
|
||||
|
||||
/// Number of values currently in the cache.
|
||||
fn len(&self) -> usize;
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
//! Computing the same thing each frame can be expensive,
|
||||
//! so often you want to save the result from the previous frame and reuse it.
|
||||
//!
|
||||
//! Enter [`FrameCache`]: it caches the results of a computation for one frame.
|
||||
//! If it is still used next frame, it is not recomputed.
|
||||
//! If it is not used next frame, it is evicted from the cache to save memory.
|
||||
use super::CacheTrait;
|
||||
|
||||
/// Something that does an expensive computation that we want to cache
|
||||
/// to save us from recomputing it each frame.
|
||||
@@ -74,17 +69,6 @@ impl<Value, Computer> FrameCache<Value, Computer> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub trait CacheTrait: 'static + Send + Sync {
|
||||
/// Call once per frame to evict cache.
|
||||
fn update(&mut self);
|
||||
|
||||
/// Number of values currently in the cache.
|
||||
fn len(&self) -> usize;
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
|
||||
}
|
||||
|
||||
impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
|
||||
for FrameCache<Value, Computer>
|
||||
{
|
||||
@@ -100,65 +84,3 @@ impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// use egui::util::cache::{CacheStorage, ComputerMut, FrameCache};
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct CharCounter {}
|
||||
/// impl ComputerMut<&str, usize> for CharCounter {
|
||||
/// fn compute(&mut self, s: &str) -> usize {
|
||||
/// s.chars().count()
|
||||
/// }
|
||||
/// }
|
||||
/// type CharCountCache<'a> = FrameCache<usize, CharCounter>;
|
||||
///
|
||||
/// # let mut cache_storage = CacheStorage::default();
|
||||
/// let mut cache = cache_storage.cache::<CharCountCache<'_>>();
|
||||
/// assert_eq!(cache.get("hello"), 5);
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
pub struct CacheStorage {
|
||||
caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
|
||||
}
|
||||
|
||||
impl CacheStorage {
|
||||
pub fn cache<FrameCache: CacheTrait + Default>(&mut self) -> &mut FrameCache {
|
||||
self.caches
|
||||
.entry(std::any::TypeId::of::<FrameCache>())
|
||||
.or_insert_with(|| Box::<FrameCache>::default())
|
||||
.as_any_mut()
|
||||
.downcast_mut::<FrameCache>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Total number of cached values
|
||||
fn num_values(&self) -> usize {
|
||||
self.caches.values().map(|cache| cache.len()).sum()
|
||||
}
|
||||
|
||||
/// Call once per frame to evict cache.
|
||||
pub fn update(&mut self) {
|
||||
for cache in self.caches.values_mut() {
|
||||
cache.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for CacheStorage {
|
||||
fn clone(&self) -> Self {
|
||||
// We return an empty cache that can be filled in again.
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CacheStorage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"FrameCacheStorage[{} caches with {} elements]",
|
||||
self.caches.len(),
|
||||
self.num_values()
|
||||
)
|
||||
}
|
||||
}
|
||||
61
crates/egui/src/cache/frame_publisher.rs
vendored
Normal file
61
crates/egui/src/cache/frame_publisher.rs
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::hash::Hash;
|
||||
|
||||
use super::CacheTrait;
|
||||
|
||||
/// Stores a key:value pair for the duration of this frame and the next.
|
||||
pub struct FramePublisher<Key: Eq + Hash, Value> {
|
||||
generation: u32,
|
||||
cache: ahash::HashMap<Key, (u32, Value)>,
|
||||
}
|
||||
|
||||
impl<Key: Eq + Hash, Value> Default for FramePublisher<Key, Value> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Key: Eq + Hash, Value> FramePublisher<Key, Value> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
generation: 0,
|
||||
cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish the value. It will be available for the duration of this and the next frame.
|
||||
pub fn set(&mut self, key: Key, value: Value) {
|
||||
self.cache.insert(key, (self.generation, value));
|
||||
}
|
||||
|
||||
/// Retrieve a value if it was published this or the previous frame.
|
||||
pub fn get(&self, key: &Key) -> Option<&Value> {
|
||||
self.cache.get(key).map(|(_, value)| value)
|
||||
}
|
||||
|
||||
/// Must be called once per frame to clear the cache.
|
||||
pub fn evict_cache(&mut self) {
|
||||
let current_generation = self.generation;
|
||||
self.cache.retain(|_key, cached| {
|
||||
cached.0 == current_generation // only keep those that were published this frame
|
||||
});
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Key, Value> CacheTrait for FramePublisher<Key, Value>
|
||||
where
|
||||
Key: 'static + Eq + Hash + Send + Sync,
|
||||
Value: 'static + Send + Sync,
|
||||
{
|
||||
fn update(&mut self) {
|
||||
self.evict_cache();
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.cache.len()
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
21
crates/egui/src/cache/mod.rs
vendored
Normal file
21
crates/egui/src/cache/mod.rs
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
//! Caches for preventing the same value from being recomputed every frame.
|
||||
//!
|
||||
//! Computing the same thing each frame can be expensive,
|
||||
//! so often you want to save the result from the previous frame and reuse it.
|
||||
//!
|
||||
//! Enter [`FrameCache`]: it caches the results of a computation for one frame.
|
||||
//! If it is still used next frame, it is not recomputed.
|
||||
//! If it is not used next frame, it is evicted from the cache to save memory.
|
||||
//!
|
||||
//! You can access egui's caches via [`crate::Memory::caches`],
|
||||
//! found with [`crate::Context::memory_mut`].
|
||||
|
||||
mod cache_storage;
|
||||
mod cache_trait;
|
||||
mod frame_cache;
|
||||
mod frame_publisher;
|
||||
|
||||
pub use cache_storage::CacheStorage;
|
||||
pub use cache_trait::CacheTrait;
|
||||
pub use frame_cache::{ComputerMut, FrameCache};
|
||||
pub use frame_publisher::FramePublisher;
|
||||
@@ -6,6 +6,7 @@ pub(crate) mod area;
|
||||
pub mod collapsing_header;
|
||||
mod combo_box;
|
||||
pub mod frame;
|
||||
pub mod modal;
|
||||
pub mod panel;
|
||||
pub mod popup;
|
||||
pub(crate) mod resize;
|
||||
@@ -18,6 +19,7 @@ pub use {
|
||||
collapsing_header::{CollapsingHeader, CollapsingResponse},
|
||||
combo_box::*,
|
||||
frame::Frame,
|
||||
modal::{Modal, ModalResponse},
|
||||
panel::{CentralPanel, SidePanel, TopBottomPanel},
|
||||
popup::*,
|
||||
resize::Resize,
|
||||
|
||||
165
crates/egui/src/containers/modal.rs
Normal file
165
crates/egui/src/containers/modal.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use crate::{
|
||||
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
|
||||
};
|
||||
use emath::{Align2, Vec2};
|
||||
|
||||
/// A modal dialog.
|
||||
/// Similar to a [`crate::Window`] but centered and with a backdrop that
|
||||
/// blocks input to the rest of the UI.
|
||||
///
|
||||
/// You can show multiple modals on top of each other. The topmost modal will always be
|
||||
/// the most recently shown one.
|
||||
pub struct Modal {
|
||||
pub area: Area,
|
||||
pub backdrop_color: Color32,
|
||||
pub frame: Option<Frame>,
|
||||
}
|
||||
|
||||
impl Modal {
|
||||
/// Create a new Modal. The id is passed to the area.
|
||||
pub fn new(id: Id) -> Self {
|
||||
Self {
|
||||
area: Self::default_area(id),
|
||||
backdrop_color: Color32::from_black_alpha(100),
|
||||
frame: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an area customized for a modal.
|
||||
/// Makes these changes to the default area:
|
||||
/// - sense: hover
|
||||
/// - anchor: center
|
||||
/// - order: foreground
|
||||
pub fn default_area(id: Id) -> Area {
|
||||
Area::new(id)
|
||||
.kind(UiKind::Modal)
|
||||
.sense(Sense::hover())
|
||||
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
|
||||
.order(Order::Foreground)
|
||||
.interactable(true)
|
||||
}
|
||||
|
||||
/// Set the frame of the modal.
|
||||
///
|
||||
/// Default is [`Frame::popup`].
|
||||
#[inline]
|
||||
pub fn frame(mut self, frame: Frame) -> Self {
|
||||
self.frame = Some(frame);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the backdrop color of the modal.
|
||||
///
|
||||
/// Default is `Color32::from_black_alpha(100)`.
|
||||
#[inline]
|
||||
pub fn backdrop_color(mut self, color: Color32) -> Self {
|
||||
self.backdrop_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the area of the modal.
|
||||
///
|
||||
/// Default is [`Modal::default_area`].
|
||||
#[inline]
|
||||
pub fn area(mut self, area: Area) -> Self {
|
||||
self.area = area;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show the modal.
|
||||
pub fn show<T>(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse<T> {
|
||||
let Self {
|
||||
area,
|
||||
backdrop_color,
|
||||
frame,
|
||||
} = self;
|
||||
|
||||
let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| {
|
||||
mem.set_modal_layer(area.layer());
|
||||
(
|
||||
mem.top_modal_layer() == Some(area.layer()),
|
||||
mem.any_popup_open(),
|
||||
)
|
||||
});
|
||||
let InnerResponse {
|
||||
inner: (inner, backdrop_response),
|
||||
response,
|
||||
} = area.show(ctx, |ui| {
|
||||
let bg_rect = ui.ctx().screen_rect();
|
||||
let bg_sense = Sense {
|
||||
click: true,
|
||||
drag: true,
|
||||
focusable: false,
|
||||
};
|
||||
let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
|
||||
backdrop.set_min_size(bg_rect.size());
|
||||
ui.painter().rect_filled(bg_rect, 0.0, backdrop_color);
|
||||
let backdrop_response = backdrop.response();
|
||||
|
||||
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
|
||||
|
||||
// We need the extra scope with the sense since frame can't have a sense and since we
|
||||
// need to prevent the clicks from passing through to the backdrop.
|
||||
let inner = ui
|
||||
.scope_builder(
|
||||
UiBuilder::new().sense(Sense {
|
||||
click: true,
|
||||
drag: true,
|
||||
focusable: false,
|
||||
}),
|
||||
|ui| frame.show(ui, content).inner,
|
||||
)
|
||||
.inner;
|
||||
|
||||
(inner, backdrop_response)
|
||||
});
|
||||
|
||||
ModalResponse {
|
||||
response,
|
||||
backdrop_response,
|
||||
inner,
|
||||
is_top_modal,
|
||||
any_popup_open,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The response of a modal dialog.
|
||||
pub struct ModalResponse<T> {
|
||||
/// The response of the modal contents
|
||||
pub response: Response,
|
||||
|
||||
/// The response of the modal backdrop.
|
||||
///
|
||||
/// A click on this means the user clicked outside the modal,
|
||||
/// in which case you might want to close the modal.
|
||||
pub backdrop_response: Response,
|
||||
|
||||
/// The inner response from the content closure
|
||||
pub inner: T,
|
||||
|
||||
/// Is this the topmost modal?
|
||||
pub is_top_modal: bool,
|
||||
|
||||
/// Is there any popup open?
|
||||
/// We need to check this before the modal contents are shown, so we can know if any popup
|
||||
/// was open when checking if the escape key was clicked.
|
||||
pub any_popup_open: bool,
|
||||
}
|
||||
|
||||
impl<T> ModalResponse<T> {
|
||||
/// Should the modal be closed?
|
||||
/// Returns true if:
|
||||
/// - the backdrop was clicked
|
||||
/// - this is the topmost modal, no popup is open and the escape key was pressed
|
||||
pub fn should_close(&self) -> bool {
|
||||
let ctx = &self.response.ctx;
|
||||
|
||||
// this is a closure so that `Esc` is consumed only if the modal is topmost
|
||||
let escape_clicked =
|
||||
|| ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
|
||||
|
||||
self.backdrop_response.clicked()
|
||||
|| (self.is_top_modal && !self.any_popup_open && escape_clicked())
|
||||
}
|
||||
}
|
||||
@@ -87,17 +87,22 @@ pub fn show_tooltip_at_pointer<R>(
|
||||
|
||||
// Add a small exclusion zone around the pointer to avoid tooltips
|
||||
// covering what we're hovering over.
|
||||
let mut exclusion_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0));
|
||||
let mut pointer_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0));
|
||||
|
||||
// Keep the left edge of the tooltip in line with the cursor:
|
||||
exclusion_rect.min.x = pointer_pos.x;
|
||||
pointer_rect.min.x = pointer_pos.x;
|
||||
|
||||
// Transform global coords to layer coords:
|
||||
if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
|
||||
pointer_rect = transform.inverse() * pointer_rect;
|
||||
}
|
||||
|
||||
show_tooltip_at_dyn(
|
||||
ctx,
|
||||
parent_layer,
|
||||
widget_id,
|
||||
allow_placing_below,
|
||||
&exclusion_rect,
|
||||
&pointer_rect,
|
||||
Box::new(add_contents),
|
||||
)
|
||||
})
|
||||
@@ -155,6 +160,7 @@ fn show_tooltip_at_dyn<'c, R>(
|
||||
widget_rect: &Rect,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
) -> R {
|
||||
// Transform layer coords to global coords:
|
||||
let mut widget_rect = *widget_rect;
|
||||
if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) {
|
||||
widget_rect = transform * widget_rect;
|
||||
|
||||
@@ -39,7 +39,7 @@ pub struct State {
|
||||
scroll_start_offset_from_top_left: [Option<f32>; 2],
|
||||
|
||||
/// Is the scroll sticky. This is true while scroll handle is in the end position
|
||||
/// and remains that way until the user moves the scroll_handle. Once unstuck (false)
|
||||
/// and remains that way until the user moves the `scroll_handle`. Once unstuck (false)
|
||||
/// it remains false until the scroll touches the end position, which reenables stickiness.
|
||||
scroll_stuck_to_end: Vec2b,
|
||||
|
||||
@@ -499,6 +499,11 @@ struct Prepared {
|
||||
|
||||
scrolling_enabled: bool,
|
||||
stick_to_end: Vec2b,
|
||||
|
||||
/// If there was a scroll target before the [`ScrollArea`] was added this frame, it's
|
||||
/// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
|
||||
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
|
||||
|
||||
animated: bool,
|
||||
}
|
||||
|
||||
@@ -693,6 +698,10 @@ impl ScrollArea {
|
||||
}
|
||||
}
|
||||
|
||||
let saved_scroll_target = content_ui
|
||||
.ctx()
|
||||
.pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
|
||||
|
||||
Prepared {
|
||||
id,
|
||||
state,
|
||||
@@ -707,6 +716,7 @@ impl ScrollArea {
|
||||
viewport,
|
||||
scrolling_enabled,
|
||||
stick_to_end,
|
||||
saved_scroll_target,
|
||||
animated,
|
||||
}
|
||||
}
|
||||
@@ -820,6 +830,7 @@ impl Prepared {
|
||||
viewport: _,
|
||||
scrolling_enabled,
|
||||
stick_to_end,
|
||||
saved_scroll_target,
|
||||
animated,
|
||||
} = self;
|
||||
|
||||
@@ -853,7 +864,7 @@ impl Prepared {
|
||||
let (start, end) = (range.min, range.max);
|
||||
let clip_start = clip_rect.min[d];
|
||||
let clip_end = clip_rect.max[d];
|
||||
let mut spacing = ui.spacing().item_spacing[d];
|
||||
let mut spacing = content_ui.spacing().item_spacing[d];
|
||||
|
||||
let delta_update = if let Some(align) = align {
|
||||
let center_factor = align.to_factor();
|
||||
@@ -902,6 +913,15 @@ impl Prepared {
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll target meant for ScrollAreas up the stack (if any)
|
||||
ui.ctx().pass_state_mut(|state| {
|
||||
for d in 0..2 {
|
||||
if saved_scroll_target[d].is_some() {
|
||||
state.scroll_target[d] = saved_scroll_target[d].clone();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let inner_rect = {
|
||||
// At this point this is the available size for the inner rect.
|
||||
let mut inner_size = inner_rect.size();
|
||||
|
||||
@@ -1160,7 +1160,8 @@ impl Context {
|
||||
/// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response {
|
||||
let interested_in_focus = w.enabled && w.sense.focusable && w.layer_id.allow_interaction();
|
||||
let interested_in_focus =
|
||||
w.enabled && w.sense.focusable && self.memory(|mem| mem.allows_interaction(w.layer_id));
|
||||
|
||||
// Remember this widget
|
||||
self.write(|ctx| {
|
||||
@@ -1172,7 +1173,7 @@ impl Context {
|
||||
viewport.this_pass.widgets.insert(w.layer_id, w);
|
||||
|
||||
if allow_focus && interested_in_focus {
|
||||
ctx.memory.interested_in_focus(w.id);
|
||||
ctx.memory.interested_in_focus(w.id, w.layer_id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3454,15 +3455,23 @@ impl Context {
|
||||
return Err(load::LoadError::NoImageLoaders);
|
||||
}
|
||||
|
||||
let mut format = None;
|
||||
|
||||
// Try most recently added loaders first (hence `.rev()`)
|
||||
for loader in image_loaders.iter().rev() {
|
||||
match loader.load(self, uri, size_hint) {
|
||||
Err(load::LoadError::NotSupported) => continue,
|
||||
Err(load::LoadError::FormatNotSupported { detected_format }) => {
|
||||
format = format.or(detected_format);
|
||||
continue;
|
||||
}
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
|
||||
Err(load::LoadError::NoMatchingImageLoader)
|
||||
Err(load::LoadError::NoMatchingImageLoader {
|
||||
detected_format: format,
|
||||
})
|
||||
}
|
||||
|
||||
/// Try loading the texture from the given uri using any available texture loaders.
|
||||
|
||||
@@ -529,6 +529,10 @@ pub enum Event {
|
||||
/// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`].
|
||||
Screenshot {
|
||||
viewport_id: crate::ViewportId,
|
||||
|
||||
/// Whatever was passed to [`crate::ViewportCommand::Screenshot`].
|
||||
user_data: crate::UserData,
|
||||
|
||||
image: std::sync::Arc<ColorImage>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ pub enum Key {
|
||||
// `]`
|
||||
CloseBracket,
|
||||
|
||||
/// \`, also known as "backquote" or "grave"
|
||||
/// Also known as "backquote" or "grave"
|
||||
Backtick,
|
||||
|
||||
/// `-`
|
||||
|
||||
@@ -3,5 +3,7 @@
|
||||
pub mod input;
|
||||
mod key;
|
||||
pub mod output;
|
||||
mod user_data;
|
||||
|
||||
pub use key::Key;
|
||||
pub use user_data::UserData;
|
||||
|
||||
74
crates/egui/src/data/user_data.rs
Normal file
74
crates/egui/src/data/user_data.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
/// A wrapper around `dyn Any`, used for passing custom user data
|
||||
/// to [`crate::ViewportCommand::Screenshot`].
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct UserData {
|
||||
/// A user value given to the screenshot command,
|
||||
/// that will be returned in [`crate::Event::Screenshot`].
|
||||
pub data: Option<Arc<dyn Any + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl UserData {
|
||||
/// You can also use [`Self::default`].
|
||||
pub fn new(user_info: impl Any + Send + Sync) -> Self {
|
||||
Self {
|
||||
data: Some(Arc::new(user_info)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for UserData {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (&self.data, &other.data) {
|
||||
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for UserData {}
|
||||
|
||||
impl std::hash::Hash for UserData {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.data.as_ref().map(Arc::as_ptr).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for UserData {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_none() // can't serialize an `Any`
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for UserData {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct UserDataVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for UserDataVisitor {
|
||||
type Value = UserData;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("a None value")
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<UserData, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(UserData::default())
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_option(UserDataVisitor)
|
||||
}
|
||||
}
|
||||
@@ -23,22 +23,30 @@ pub struct DragAndDrop {
|
||||
|
||||
impl DragAndDrop {
|
||||
pub(crate) fn register(ctx: &Context) {
|
||||
ctx.on_end_pass("debug_text", std::sync::Arc::new(Self::end_pass));
|
||||
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));
|
||||
}
|
||||
|
||||
fn begin_pass(ctx: &Context) {
|
||||
let has_any_payload = Self::has_any_payload(ctx);
|
||||
|
||||
if has_any_payload {
|
||||
let abort_dnd = ctx.input_mut(|i| {
|
||||
i.pointer.any_released()
|
||||
|| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape)
|
||||
});
|
||||
|
||||
if abort_dnd {
|
||||
Self::clear_payload(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn end_pass(ctx: &Context) {
|
||||
let abort_dnd =
|
||||
ctx.input(|i| i.pointer.any_released() || i.key_pressed(crate::Key::Escape));
|
||||
|
||||
let mut is_dragging = false;
|
||||
|
||||
ctx.data_mut(|data| {
|
||||
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
|
||||
|
||||
if abort_dnd {
|
||||
state.payload = None;
|
||||
}
|
||||
|
||||
is_dragging = state.payload.is_some();
|
||||
});
|
||||
|
||||
|
||||
@@ -889,9 +889,9 @@ impl Default for PointerState {
|
||||
press_start_time: None,
|
||||
has_moved_too_much_for_a_click: false,
|
||||
started_decidedly_dragging: false,
|
||||
last_click_time: std::f64::NEG_INFINITY,
|
||||
last_last_click_time: std::f64::NEG_INFINITY,
|
||||
last_move_time: std::f64::NEG_INFINITY,
|
||||
last_click_time: f64::NEG_INFINITY,
|
||||
last_last_click_time: f64::NEG_INFINITY,
|
||||
last_move_time: f64::NEG_INFINITY,
|
||||
pointer_events: vec![],
|
||||
input_options: Default::default(),
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ impl LayerId {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[deprecated = "Use `Memory::allows_interaction` instead"]
|
||||
pub fn allow_interaction(&self) -> bool {
|
||||
self.order.allow_interaction()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2},
|
||||
Align,
|
||||
};
|
||||
use std::f32::INFINITY;
|
||||
const INFINITY: f32 = f32::INFINITY;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
|
||||
//!
|
||||
//! `egui` is in heavy development, with each new version having breaking changes.
|
||||
//! You need to have rust 1.77.0 or later to use `egui`.
|
||||
//! You need to have rust 1.79.0 or later to use `egui`.
|
||||
//!
|
||||
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
|
||||
//! which uses [`eframe`](https://docs.rs/eframe).
|
||||
@@ -393,6 +393,7 @@
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
|
||||
mod animation_manager;
|
||||
pub mod cache;
|
||||
pub mod containers;
|
||||
mod context;
|
||||
mod data;
|
||||
@@ -471,7 +472,7 @@ pub use self::{
|
||||
output::{
|
||||
self, CursorIcon, FullOutput, OpenUrl, PlatformOutput, UserAttentionType, WidgetInfo,
|
||||
},
|
||||
Key,
|
||||
Key, UserData,
|
||||
},
|
||||
drag_and_drop::DragAndDrop,
|
||||
epaint::text::TextWrapMode,
|
||||
|
||||
@@ -77,16 +77,19 @@ pub enum LoadError {
|
||||
/// Programmer error: There are no image loaders installed.
|
||||
NoImageLoaders,
|
||||
|
||||
/// A specific loader does not support this scheme, protocol or image format.
|
||||
/// A specific loader does not support this scheme or protocol.
|
||||
NotSupported,
|
||||
|
||||
/// A specific loader does not support the format of the image.
|
||||
FormatNotSupported { detected_format: Option<String> },
|
||||
|
||||
/// Programmer error: Failed to find the bytes for this image because
|
||||
/// there was no [`BytesLoader`] supporting the scheme.
|
||||
NoMatchingBytesLoader,
|
||||
|
||||
/// Programmer error: Failed to parse the bytes as an image because
|
||||
/// there was no [`ImageLoader`] supporting the scheme.
|
||||
NoMatchingImageLoader,
|
||||
/// there was no [`ImageLoader`] supporting the format.
|
||||
NoMatchingImageLoader { detected_format: Option<String> },
|
||||
|
||||
/// Programmer error: no matching [`TextureLoader`].
|
||||
/// Because of the [`DefaultTextureLoader`], this error should never happen.
|
||||
@@ -96,6 +99,20 @@ pub enum LoadError {
|
||||
Loading(String),
|
||||
}
|
||||
|
||||
impl LoadError {
|
||||
/// Returns the (approximate) size of the error message in bytes.
|
||||
pub fn byte_size(&self) -> usize {
|
||||
match self {
|
||||
Self::FormatNotSupported { detected_format }
|
||||
| Self::NoMatchingImageLoader { detected_format } => {
|
||||
detected_format.as_ref().map_or(0, |s| s.len())
|
||||
}
|
||||
Self::Loading(message) => message.len(),
|
||||
_ => std::mem::size_of::<Self>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LoadError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -105,12 +122,15 @@ impl Display for LoadError {
|
||||
|
||||
Self::NoMatchingBytesLoader => f.write_str("No matching BytesLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."),
|
||||
|
||||
Self::NoMatchingImageLoader => f.write_str("No matching ImageLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."),
|
||||
Self::NoMatchingImageLoader { detected_format: None } => f.write_str("No matching ImageLoader. Either no ImageLoader is installed or the image is corrupted / has an unsupported format."),
|
||||
Self::NoMatchingImageLoader { detected_format: Some(detected_format) } => write!(f, "No matching ImageLoader for format: {detected_format:?}. Make sure you enabled the necessary features on the image crate."),
|
||||
|
||||
Self::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"),
|
||||
|
||||
Self::NotSupported => f.write_str("Image scheme or URI not supported by this loader"),
|
||||
|
||||
Self::FormatNotSupported { detected_format } => write!(f, "Image format not supported by this loader: {detected_format:?}"),
|
||||
|
||||
Self::Loading(message) => f.write_str(message),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ pub struct Memory {
|
||||
/// so as not to lock the UI thread.
|
||||
///
|
||||
/// ```
|
||||
/// use egui::util::cache::{ComputerMut, FrameCache};
|
||||
/// use egui::cache::{ComputerMut, FrameCache};
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct CharCounter {}
|
||||
@@ -72,7 +72,7 @@ pub struct Memory {
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
pub caches: crate::util::cache::CacheStorage,
|
||||
pub caches: crate::cache::CacheStorage,
|
||||
|
||||
// ------------------------------------------
|
||||
/// new fonts that will be applied at the start of the next frame
|
||||
@@ -513,6 +513,12 @@ pub(crate) struct Focus {
|
||||
/// Set when looking for widget with navigational keys like arrows, tab, shift+tab.
|
||||
focus_direction: FocusDirection,
|
||||
|
||||
/// The top-most modal layer from the previous frame.
|
||||
top_modal_layer: Option<LayerId>,
|
||||
|
||||
/// The top-most modal layer from the current frame.
|
||||
top_modal_layer_current_frame: Option<LayerId>,
|
||||
|
||||
/// A cache of widget IDs that are interested in focus with their corresponding rectangles.
|
||||
focus_widgets_cache: IdMap<Rect>,
|
||||
}
|
||||
@@ -623,6 +629,8 @@ impl Focus {
|
||||
self.focused_widget = None;
|
||||
}
|
||||
}
|
||||
|
||||
self.top_modal_layer = self.top_modal_layer_current_frame.take();
|
||||
}
|
||||
|
||||
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
|
||||
@@ -676,6 +684,14 @@ impl Focus {
|
||||
self.last_interested = Some(id);
|
||||
}
|
||||
|
||||
fn set_modal_layer(&mut self, layer_id: LayerId) {
|
||||
self.top_modal_layer_current_frame = Some(layer_id);
|
||||
}
|
||||
|
||||
pub(crate) fn top_modal_layer(&self) -> Option<LayerId> {
|
||||
self.top_modal_layer
|
||||
}
|
||||
|
||||
fn reset_focus(&mut self) {
|
||||
self.focus_direction = FocusDirection::None;
|
||||
}
|
||||
@@ -720,7 +736,7 @@ impl Focus {
|
||||
|
||||
let current_rect = self.focus_widgets_cache.get(¤t_focused.id)?;
|
||||
|
||||
let mut best_score = std::f32::INFINITY;
|
||||
let mut best_score = f32::INFINITY;
|
||||
let mut best_id = None;
|
||||
|
||||
for (candidate_id, candidate_rect) in &self.focus_widgets_cache {
|
||||
@@ -802,7 +818,15 @@ impl Memory {
|
||||
|
||||
/// Top-most layer at the given position.
|
||||
pub fn layer_id_at(&self, pos: Pos2) -> Option<LayerId> {
|
||||
self.areas().layer_id_at(pos, &self.layer_transforms)
|
||||
self.areas()
|
||||
.layer_id_at(pos, &self.layer_transforms)
|
||||
.and_then(|layer_id| {
|
||||
if self.is_above_modal_layer(layer_id) {
|
||||
Some(layer_id)
|
||||
} else {
|
||||
self.top_modal_layer()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// An iterator over all layers. Back-to-front, top is last.
|
||||
@@ -877,6 +901,30 @@ impl Memory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if
|
||||
/// - this layer is the top-most modal layer or above it
|
||||
/// - there is no modal layer
|
||||
pub fn is_above_modal_layer(&self, layer_id: LayerId) -> bool {
|
||||
if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) {
|
||||
matches!(
|
||||
self.areas().compare_order(layer_id, modal_layer),
|
||||
std::cmp::Ordering::Equal | std::cmp::Ordering::Greater
|
||||
)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Does this layer allow interaction?
|
||||
/// Returns true if
|
||||
/// - the layer is not behind a modal layer
|
||||
/// - the [`Order`] allows interaction
|
||||
pub fn allows_interaction(&self, layer_id: LayerId) -> bool {
|
||||
let is_above_modal_layer = self.is_above_modal_layer(layer_id);
|
||||
let ordering_allows_interaction = layer_id.order.allow_interaction();
|
||||
is_above_modal_layer && ordering_allows_interaction
|
||||
}
|
||||
|
||||
/// Register this widget as being interested in getting keyboard focus.
|
||||
/// This will allow the user to select it with tab and shift-tab.
|
||||
/// This is normally done automatically when handling interactions,
|
||||
@@ -884,11 +932,36 @@ impl Memory {
|
||||
/// e.g. before deciding which type of underlying widget to use,
|
||||
/// as in the [`crate::DragValue`] widget, so a widget can be focused
|
||||
/// and rendered correctly in a single frame.
|
||||
///
|
||||
/// Pass in the `layer_id` of the layer that the widget is in.
|
||||
#[inline(always)]
|
||||
pub fn interested_in_focus(&mut self, id: Id) {
|
||||
pub fn interested_in_focus(&mut self, id: Id, layer_id: LayerId) {
|
||||
if !self.allows_interaction(layer_id) {
|
||||
return;
|
||||
}
|
||||
self.focus_mut().interested_in_focus(id);
|
||||
}
|
||||
|
||||
/// Limit focus to widgets on the given layer and above.
|
||||
/// If this is called multiple times per frame, the top layer wins.
|
||||
pub fn set_modal_layer(&mut self, layer_id: LayerId) {
|
||||
if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame) {
|
||||
if matches!(
|
||||
self.areas().compare_order(layer_id, current),
|
||||
std::cmp::Ordering::Less
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.focus_mut().set_modal_layer(layer_id);
|
||||
}
|
||||
|
||||
/// Get the top modal layer (from the previous frame).
|
||||
pub fn top_modal_layer(&self) -> Option<LayerId> {
|
||||
self.focus()?.top_modal_layer()
|
||||
}
|
||||
|
||||
/// Stop editing the active [`TextEdit`](crate::TextEdit) (if any).
|
||||
#[inline(always)]
|
||||
pub fn stop_text_input(&mut self) {
|
||||
@@ -1037,6 +1110,9 @@ impl Memory {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Map containing the index of each layer in the order list, for quick lookups.
|
||||
type OrderMap = HashMap<LayerId, usize>;
|
||||
|
||||
/// Keeps track of [`Area`](crate::containers::area::Area)s, which are free-floating [`Ui`](crate::Ui)s.
|
||||
/// These [`Area`](crate::containers::area::Area)s can be in any [`Order`].
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -1048,6 +1124,9 @@ pub struct Areas {
|
||||
/// Back-to-front, top is last.
|
||||
order: Vec<LayerId>,
|
||||
|
||||
/// Actual order of the layers, pre-calculated each frame.
|
||||
order_map: OrderMap,
|
||||
|
||||
visible_last_frame: ahash::HashSet<LayerId>,
|
||||
visible_current_frame: ahash::HashSet<LayerId>,
|
||||
|
||||
@@ -1079,12 +1158,28 @@ impl Areas {
|
||||
}
|
||||
|
||||
/// For each layer, which [`Self::order`] is it in?
|
||||
pub(crate) fn order_map(&self) -> HashMap<LayerId, usize> {
|
||||
self.order
|
||||
pub(crate) fn order_map(&self) -> &OrderMap {
|
||||
&self.order_map
|
||||
}
|
||||
|
||||
/// Compare the order of two layers, based on the order list from last frame.
|
||||
/// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list.
|
||||
pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering {
|
||||
if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) {
|
||||
a.cmp(b)
|
||||
} else {
|
||||
a.order.cmp(&b.order)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the order map.
|
||||
fn calculate_order_map(&mut self) {
|
||||
self.order_map = self
|
||||
.order
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, id)| (*id, i))
|
||||
.collect()
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) {
|
||||
@@ -1209,6 +1304,7 @@ impl Areas {
|
||||
};
|
||||
order.splice(parent_pos..=parent_pos, moved_layers);
|
||||
}
|
||||
self.calculate_order_map();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ pub struct Style {
|
||||
/// If true and scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift
|
||||
pub always_scroll_the_only_direction: bool,
|
||||
|
||||
/// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [Ui::scroll_to_rect].
|
||||
/// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [`Ui::scroll_to_rect`].
|
||||
pub scroll_animation: ScrollAnimation,
|
||||
}
|
||||
|
||||
|
||||
@@ -97,8 +97,19 @@ pub fn paint_text_selection(
|
||||
pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
|
||||
let stroke = visuals.text_cursor.stroke;
|
||||
|
||||
let top = cursor_rect.center_top();
|
||||
let bottom = cursor_rect.center_bottom();
|
||||
// Ensure the cursor is aligned to the pixel grid for whole number widths.
|
||||
// See https://github.com/emilk/egui/issues/5164
|
||||
let (top, bottom) = if (stroke.width as usize) % 2 == 0 {
|
||||
(
|
||||
painter.round_pos_to_pixels(cursor_rect.center_top()),
|
||||
painter.round_pos_to_pixels(cursor_rect.center_bottom()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
painter.round_pos_to_pixel_center(cursor_rect.center_top()),
|
||||
painter.round_pos_to_pixel_center(cursor_rect.center_bottom()),
|
||||
)
|
||||
};
|
||||
|
||||
painter.line_segment([top, bottom], (stroke.width, stroke.color));
|
||||
|
||||
@@ -122,14 +133,14 @@ pub fn paint_text_cursor(
|
||||
ui: &Ui,
|
||||
painter: &Painter,
|
||||
primary_cursor_rect: Rect,
|
||||
time_since_last_edit: f64,
|
||||
time_since_last_interaction: f64,
|
||||
) {
|
||||
if ui.visuals().text_cursor.blink {
|
||||
let on_duration = ui.visuals().text_cursor.on_duration;
|
||||
let off_duration = ui.visuals().text_cursor.off_duration;
|
||||
let total_duration = on_duration + off_duration;
|
||||
|
||||
let time_in_cycle = (time_since_last_edit % (total_duration as f64)) as f32;
|
||||
let time_in_cycle = (time_since_last_interaction % (total_duration as f64)) as f32;
|
||||
|
||||
let wake_in = if time_in_cycle < on_duration {
|
||||
// Cursor is visible
|
||||
|
||||
@@ -24,6 +24,9 @@ pub enum UiKind {
|
||||
/// A bottom [`crate::TopBottomPanel`].
|
||||
BottomPanel,
|
||||
|
||||
/// A modal [`crate::Modal`].
|
||||
Modal,
|
||||
|
||||
/// A [`crate::Frame`].
|
||||
Frame,
|
||||
|
||||
@@ -82,6 +85,7 @@ impl UiKind {
|
||||
|
||||
Self::Window
|
||||
| Self::Menu
|
||||
| Self::Modal
|
||||
| Self::Popup
|
||||
| Self::Tooltip
|
||||
| Self::Picker
|
||||
@@ -228,6 +232,12 @@ impl UiStack {
|
||||
self.kind().map_or(false, |kind| kind.is_panel())
|
||||
}
|
||||
|
||||
/// Is this [`crate::Ui`] an [`crate::Area`]?
|
||||
#[inline]
|
||||
pub fn is_area_ui(&self) -> bool {
|
||||
self.kind().map_or(false, |kind| kind.is_area())
|
||||
}
|
||||
|
||||
/// Is this a root [`crate::Ui`], i.e. created with [`crate::Ui::new()`]?
|
||||
#[inline]
|
||||
pub fn is_root_ui(&self) -> bool {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Miscellaneous tools used by the rest of egui.
|
||||
|
||||
pub mod cache;
|
||||
pub(crate) mod fixed_cache;
|
||||
pub mod id_type_map;
|
||||
pub mod undoer;
|
||||
@@ -9,3 +8,7 @@ pub use id_type_map::IdTypeMap;
|
||||
|
||||
pub use epaint::emath::History;
|
||||
pub use epaint::util::{hash, hash_with};
|
||||
|
||||
/// Deprecated alias for [`crate::cache`].
|
||||
#[deprecated = "Use egui::cache instead"]
|
||||
pub use crate::cache;
|
||||
|
||||
@@ -1058,8 +1058,8 @@ pub enum ViewportCommand {
|
||||
|
||||
/// Take a screenshot.
|
||||
///
|
||||
/// The results are returned in `crate::Event::Screenshot`.
|
||||
Screenshot,
|
||||
/// The results are returned in [`crate::Event::Screenshot`].
|
||||
Screenshot(crate::UserData),
|
||||
|
||||
/// Request cut of the current selection
|
||||
///
|
||||
@@ -1100,6 +1100,8 @@ impl ViewportCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Describes a viewport, i.e. a native window.
|
||||
///
|
||||
/// This is returned by [`crate::Context::run`] on each frame, and should be applied
|
||||
|
||||
@@ -37,6 +37,7 @@ pub struct Button<'a> {
|
||||
min_size: Vec2,
|
||||
rounding: Option<Rounding>,
|
||||
selected: bool,
|
||||
image_tint_follows_text_color: bool,
|
||||
}
|
||||
|
||||
impl<'a> Button<'a> {
|
||||
@@ -70,6 +71,7 @@ impl<'a> Button<'a> {
|
||||
min_size: Vec2::ZERO,
|
||||
rounding: None,
|
||||
selected: false,
|
||||
image_tint_follows_text_color: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +158,18 @@ impl<'a> Button<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// If true, the tint of the image is multiplied by the widget text color.
|
||||
///
|
||||
/// This makes sense for images that are white, that should have the same color as the text color.
|
||||
/// This will also make the icon color depend on hover state.
|
||||
///
|
||||
/// Default: `false`.
|
||||
#[inline]
|
||||
pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
|
||||
self.image_tint_follows_text_color = image_tint_follows_text_color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show some text on the right side of the button, in weak color.
|
||||
///
|
||||
/// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`).
|
||||
@@ -190,6 +204,7 @@ impl Widget for Button<'_> {
|
||||
min_size,
|
||||
rounding,
|
||||
selected,
|
||||
image_tint_follows_text_color,
|
||||
} = self;
|
||||
|
||||
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
|
||||
@@ -319,12 +334,16 @@ impl Widget for Button<'_> {
|
||||
let image_rect = Rect::from_min_size(image_pos, image_size);
|
||||
cursor_x += image_size.x;
|
||||
let tlr = image.load_for_size(ui.ctx(), image_size);
|
||||
let mut image_options = image.image_options().clone();
|
||||
if image_tint_follows_text_color {
|
||||
image_options.tint = image_options.tint * visuals.text_color();
|
||||
}
|
||||
widgets::image::paint_texture_load_result(
|
||||
ui,
|
||||
&tlr,
|
||||
image_rect,
|
||||
image.show_loading_spinner,
|
||||
image.image_options(),
|
||||
&image_options,
|
||||
);
|
||||
response = widgets::image::texture_load_result_response(
|
||||
&image.source(ui.ctx()),
|
||||
|
||||
@@ -452,7 +452,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||
// in button mode for just one frame. This is important for
|
||||
// screen readers.
|
||||
let is_kb_editing = ui.memory_mut(|mem| {
|
||||
mem.interested_in_focus(id);
|
||||
mem.interested_in_focus(id, ui.layer_id());
|
||||
mem.has_focus(id)
|
||||
});
|
||||
|
||||
|
||||
@@ -1030,7 +1030,7 @@ impl<'a> Widget for Slider<'a> {
|
||||
// Logarithmic sliders are allowed to include zero and infinity,
|
||||
// even though mathematically it doesn't make sense.
|
||||
|
||||
use std::f64::INFINITY;
|
||||
const INFINITY: f64 = f64::INFINITY;
|
||||
|
||||
/// When the user asks for an infinitely large range (e.g. logarithmic from zero),
|
||||
/// give a scale that this many orders of magnitude in size.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use emath::Rect;
|
||||
use epaint::text::{cursor::CCursor, Galley, LayoutJob};
|
||||
|
||||
use crate::{
|
||||
@@ -602,6 +603,8 @@ impl<'t> TextEdit<'t> {
|
||||
|
||||
if did_interact || response.clicked() {
|
||||
ui.memory_mut(|mem| mem.request_focus(response.id));
|
||||
|
||||
state.last_interaction_time = ui.ctx().input(|i| i.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -720,6 +723,16 @@ impl<'t> TextEdit<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate additional space if edits were made this frame that changed the size. This is important so that,
|
||||
// if there's a ScrollArea, it can properly scroll to the cursor.
|
||||
let extra_size = galley.size() - rect.size();
|
||||
if extra_size.x > 0.0 || extra_size.y > 0.0 {
|
||||
ui.allocate_rect(
|
||||
Rect::from_min_size(outer_rect.max, extra_size),
|
||||
Sense::hover(),
|
||||
);
|
||||
}
|
||||
|
||||
painter.galley(galley_pos, galley.clone(), text_color);
|
||||
|
||||
if has_focus {
|
||||
@@ -727,16 +740,15 @@ impl<'t> TextEdit<'t> {
|
||||
let primary_cursor_rect =
|
||||
cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height);
|
||||
|
||||
let is_fully_visible = ui.clip_rect().contains_rect(rect); // TODO(emilk): remove this HACK workaround for https://github.com/emilk/egui/issues/1531
|
||||
if (response.changed || selection_changed) && !is_fully_visible {
|
||||
if response.changed || selection_changed {
|
||||
// Scroll to keep primary cursor in view:
|
||||
ui.scroll_to_rect(primary_cursor_rect, None);
|
||||
ui.scroll_to_rect(primary_cursor_rect + margin, None);
|
||||
}
|
||||
|
||||
if text.is_mutable() && interactive {
|
||||
let now = ui.ctx().input(|i| i.time);
|
||||
if response.changed || selection_changed {
|
||||
state.last_edit_time = now;
|
||||
state.last_interaction_time = now;
|
||||
}
|
||||
|
||||
// Only show (and blink) cursor if the egui viewport has focus.
|
||||
@@ -749,7 +761,7 @@ impl<'t> TextEdit<'t> {
|
||||
ui,
|
||||
&painter,
|
||||
primary_cursor_rect,
|
||||
now - state.last_edit_time,
|
||||
now - state.last_interaction_time,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,10 +53,10 @@ pub struct TextEditState {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub(crate) singleline_offset: f32,
|
||||
|
||||
/// When did the user last press a key?
|
||||
/// When did the user last press a key or click on the `TextEdit`.
|
||||
/// Used to pause the cursor animation when typing.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub(crate) last_edit_time: f64,
|
||||
pub(crate) last_interaction_time: f64,
|
||||
}
|
||||
|
||||
impl TextEditState {
|
||||
|
||||
Reference in New Issue
Block a user