1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-27 07:03:14 -04:00

Merge branch 'emilk:master' into cache_galley_lines

This commit is contained in:
Hubert Głuchowski
2024-12-07 18:44:48 +01:00
committed by GitHub
97 changed files with 1368 additions and 328 deletions

View File

@@ -9,4 +9,4 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Machete
uses: bnjbvr/cargo-machete@main
run: cargo install cargo-machete --locked && cargo machete

View File

@@ -39,7 +39,7 @@ jobs:
with:
profile: minimal
target: wasm32-unknown-unknown
toolchain: 1.77.0
toolchain: 1.79.0
override: true
- uses: Swatinem/rust-cache@v2

View File

@@ -18,7 +18,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.77.0
toolchain: 1.79.0
- name: Install packages (Linux)
if: runner.os == 'Linux'
@@ -83,7 +83,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.77.0
toolchain: 1.79.0
targets: wasm32-unknown-unknown
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
with:
rust-version: "1.77.0"
rust-version: "1.79.0"
log-level: error
command: check
arguments: --target ${{ matrix.target }}
@@ -170,7 +170,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.77.0
toolchain: 1.79.0
targets: aarch64-linux-android
- name: Set up cargo cache
@@ -189,7 +189,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.77.0
toolchain: 1.79.0
targets: aarch64-apple-ios
- name: Set up cargo cache
@@ -208,7 +208,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.77.0
toolchain: 1.79.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2
@@ -232,7 +232,7 @@ jobs:
lfs: true
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.77.0
toolchain: 1.79.0
- name: Set up cargo cache
uses: Swatinem/rust-cache@v2

View File

@@ -1 +1,2 @@
/crates/egui_kittest @lucasmerlin
/crates/egui-wgpu @Wumpf

View File

@@ -2239,7 +2239,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kittest"
version = "0.1.0"
source = "git+https://github.com/rerun-io/kittest?branch=main#63c5b7d58178900e523428ca5edecbba007a2702"
source = "git+https://github.com/rerun-io/kittest?branch=main#06e01f17fed36a997e1541f37b2d47e3771d7533"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -4356,9 +4356,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]]
name = "wgpu"
version = "23.0.0"
version = "23.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ab52f2d3d18b70d5ab8dd270a1cff3ebe6dbe4a7d13c1cc2557138a9777fdc"
checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a"
dependencies = [
"arrayvec",
"cfg_aliases 0.1.1",
@@ -4381,9 +4381,9 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "23.0.0"
version = "23.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c68e7b6322a03ee5b83fcd92caeac5c2a932f6457818179f4652ad2a9c065"
checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a"
dependencies = [
"arrayvec",
"bit-vec 0.8.0",
@@ -4406,9 +4406,9 @@ dependencies = [
[[package]]
name = "wgpu-hal"
version = "23.0.0"
version = "23.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de6e7266b869de56c7e3ed72a954899f71d14fec6cc81c102b7530b92947601b"
checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821"
dependencies = [
"android_system_properties",
"arrayvec",
@@ -4478,7 +4478,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]

View File

@@ -23,7 +23,7 @@ members = [
[workspace.package]
edition = "2021"
license = "MIT OR Apache-2.0"
rust-version = "1.77"
rust-version = "1.79"
version = "0.29.1"
@@ -145,6 +145,7 @@ disallowed_types = "warn" # See clippy.toml
doc_link_with_quotes = "warn"
doc_markdown = "warn"
empty_enum = "warn"
empty_enum_variants_with_brackets = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
exit = "warn"
@@ -169,6 +170,8 @@ inefficient_to_string = "warn"
infinite_loop = "warn"
into_iter_without_iter = "warn"
invalid_upcast_comparisons = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
iter_on_empty_collections = "warn"
iter_on_single_items = "warn"
@@ -185,6 +188,7 @@ macro_use_imports = "warn"
manual_assert = "warn"
manual_clamp = "warn"
manual_instant_elapsed = "warn"
manual_is_variant_and = "warn"
manual_let_else = "warn"
manual_ok_or = "warn"
manual_string_new = "warn"
@@ -202,6 +206,7 @@ mismatching_type_param_order = "warn"
missing_enforced_import_renames = "warn"
missing_errors_doc = "warn"
missing_safety_doc = "warn"
mixed_attributes_style = "warn"
mut_mut = "warn"
mutex_integer = "warn"
needless_borrow = "warn"
@@ -211,21 +216,25 @@ needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn"
negative_feature_names = "warn"
nonstandard_macro_braces = "warn"
option_as_ref_cloned = "warn"
option_option = "warn"
path_buf_push_overwrite = "warn"
print_stderr = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
pub_underscore_fields = "warn"
pub_without_shorthand = "warn"
rc_mutex = "warn"
readonly_write_lock = "warn"
redundant_type_annotations = "warn"
ref_as_ptr = "warn"
ref_option_ref = "warn"
ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "warn"
same_functions_in_if_condition = "warn"
semicolon_if_nothing_returned = "warn"
single_match_else = "warn"
str_split_at_newline = "warn"
str_to_string = "warn"
string_add = "warn"
string_add_assign = "warn"
@@ -261,12 +270,15 @@ zero_sized_map_values = "warn"
# TODO(emilk): enable more of these lints:
iter_over_hash_type = "allow"
let_underscore_untyped = "allow"
missing_assert_message = "allow"
should_panic_without_expect = "allow"
too_many_lines = "allow"
unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one
# These are meh:
assigning_clones = "allow" # No please
let_underscore_must_use = "allow"
let_underscore_untyped = "allow"
manual_range_contains = "allow" # this one is just worse imho
self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
significant_drop_tightening = "allow" # Too many false positives

View File

@@ -3,7 +3,7 @@
# -----------------------------------------------------------------------------
# Section identical to scripts/clippy_wasm/clippy.toml:
msrv = "1.77"
msrv = "1.79"
allow-unwrap-in-tests = true
@@ -69,9 +69,12 @@ disallowed-types = [
# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
doc-valid-idents = [
# You must also update the same list in the root `clippy.toml`!
# You must also update the same list in `scripts/clippy_wasm/clippy.toml`!
"AccessKit",
"WebGL",
"WebGL1",
"WebGL2",
"WebGPU",
"VirtualBox",
"..",
]

View File

@@ -269,3 +269,18 @@ impl Color32 {
)
}
}
impl std::ops::Mul for Color32 {
type Output = Self;
/// Fast gamma-space multiplication.
#[inline]
fn mul(self, other: Self) -> Self {
Self([
fast_round(self[0] as f32 * other[0] as f32 / 255.0),
fast_round(self[1] as f32 * other[1] as f32 / 255.0),
fast_round(self[2] as f32 * other[2] as f32 / 255.0),
fast_round(self[3] as f32 * other[3] as f32 / 255.0),
])
}
}

View File

@@ -439,7 +439,7 @@ pub struct WebOptions {
/// Unused by webgl context as of writing.
pub depth_buffer: u8,
/// Which version of WebGl context to select
/// Which version of WebGL context to select
///
/// Default: [`WebGlContextOption::BestFirst`].
#[cfg(feature = "glow")]

View File

@@ -14,7 +14,7 @@ impl EventLoopGuard {
cell.get().is_none(),
"Attempted to set a new event loop while one is already set"
);
cell.set(Some(event_loop as *const ActiveEventLoop));
cell.set(Some(std::ptr::from_ref::<ActiveEventLoop>(event_loop)));
});
Self
}

View File

@@ -661,13 +661,14 @@ impl<'app> GlowWinitRunning<'app> {
{
for action in viewport.actions_requested.drain() {
match action {
ActionRequested::Screenshot => {
ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.into(),
});
}
@@ -943,7 +944,7 @@ impl GlutinWindowContext {
// we might want to expose this option to users in the future. maybe using an env var or using native_options.
//
// The justification for FallbackEgl over PreferEgl is at https://github.com/emilk/egui/pull/2526#issuecomment-1400229576 .
.with_preference(glutin_winit::ApiPreference::PreferEgl)
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
egui_ctx,
event_loop,

View File

@@ -643,10 +643,16 @@ impl<'app> WgpuWinitRunning<'app> {
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
let screenshot_requested = viewport
.actions_requested
.take(&ActionRequested::Screenshot)
.is_some();
let mut screenshot_commands = vec![];
viewport.actions_requested.retain(|cmd| {
if let ActionRequested::Screenshot(info) = cmd {
screenshot_commands.push(info.clone());
false
} else {
true
}
});
let screenshot_requested = !screenshot_commands.is_empty();
let (vsync_secs, screenshot) = painter.paint_and_update_textures(
viewport_id,
pixels_per_point,
@@ -655,19 +661,32 @@ impl<'app> WgpuWinitRunning<'app> {
&textures_delta,
screenshot_requested,
);
if let Some(screenshot) = screenshot {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
image: screenshot.into(),
});
match (screenshot_requested, screenshot) {
(false, None) => {}
(true, Some(screenshot)) => {
let screenshot = Arc::new(screenshot);
for user_data in screenshot_commands {
egui_winit
.egui_input_mut()
.events
.push(egui::Event::Screenshot {
viewport_id,
user_data,
image: screenshot.clone(),
});
}
}
(true, None) => {
log::error!("Bug in egui_wgpu: screenshot requested, but no screenshot was taken");
}
(false, Some(_)) => {
log::warn!("Bug in egui_wgpu: Got screenshot without requesting it");
}
}
for action in viewport.actions_requested.drain() {
match action {
ActionRequested::Screenshot => {
ActionRequested::Screenshot { .. } => {
// already handled above
}
ActionRequested::Cut => {

View File

@@ -1,42 +1,14 @@
use raw_window_handle::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
RawWindowHandle, WebDisplayHandle, WebWindowHandle, WindowHandle,
};
use std::sync::Arc;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use crate::WebOptions;
use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup};
use crate::WebOptions;
use super::web_painter::WebPainter;
struct EguiWebWindow(u32);
#[allow(unsafe_code)]
impl HasWindowHandle for EguiWebWindow {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
// SAFETY: there is no lifetime here.
unsafe {
Ok(WindowHandle::borrow_raw(RawWindowHandle::Web(
WebWindowHandle::new(self.0),
)))
}
}
}
#[allow(unsafe_code)]
impl HasDisplayHandle for EguiWebWindow {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
// SAFETY: there is no lifetime here.
unsafe {
Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Web(
WebDisplayHandle::new(),
)))
}
}
}
pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement,
surface: wgpu::Surface<'static>,

View File

@@ -16,7 +16,7 @@ pub struct WebRunner {
/// Have we ever panicked?
panic_handler: PanicHandler,
/// If we ever panic during running, this RefCell is poisoned.
/// If we ever panic during running, this `RefCell` is poisoned.
/// So before we use it, we need to check [`Self::panic_handler`].
runner: Rc<RefCell<Option<AppRunner>>>,

View File

@@ -125,7 +125,7 @@ pub struct ScreenDescriptor {
/// Size of the window in physical pixels.
pub size_in_pixels: [u32; 2],
/// HiDPI scale factor (pixels per point).
/// High-DPI scale factor (pixels per point).
pub pixels_per_point: f32,
}

View File

@@ -1301,7 +1301,7 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option<winit::window::Curs
// ---------------------------------------------------------------------------
#[derive(PartialEq, Eq, Hash, Debug)]
pub enum ActionRequested {
Screenshot,
Screenshot(egui::UserData),
Cut,
Copy,
Paste,
@@ -1516,8 +1516,8 @@ fn process_viewport_command(
log::warn!("{command:?}: {err}");
}
}
ViewportCommand::Screenshot => {
actions_requested.insert(ActionRequested::Screenshot);
ViewportCommand::Screenshot(user_data) => {
actions_requested.insert(ActionRequested::Screenshot(user_data));
}
ViewportCommand::RequestCut => {
actions_requested.insert(ActionRequested::Cut);

69
crates/egui/src/cache/cache_storage.rs vendored Normal file
View 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
View 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;
}

View File

@@ -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()
)
}
}

View 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
View 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;

View File

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

View 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())
}
}

View File

@@ -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;

View File

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

View File

@@ -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.

View File

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

View File

@@ -55,7 +55,7 @@ pub enum Key {
// `]`
CloseBracket,
/// \`, also known as "backquote" or "grave"
/// Also known as "backquote" or "grave"
Backtick,
/// `-`

View File

@@ -3,5 +3,7 @@
pub mod input;
mod key;
pub mod output;
mod user_data;
pub use key::Key;
pub use user_data::UserData;

View 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)
}
}

View File

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

View File

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

View File

@@ -95,6 +95,7 @@ impl LayerId {
}
#[inline(always)]
#[deprecated = "Use `Memory::allows_interaction` instead"]
pub fn allow_interaction(&self) -> bool {
self.order.allow_interaction()
}

View File

@@ -2,7 +2,7 @@ use crate::{
emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2},
Align,
};
use std::f32::INFINITY;
const INFINITY: f32 = f32::INFINITY;
// ----------------------------------------------------------------------------

View File

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

View File

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

View File

@@ -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(&current_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();
}
}

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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;

View File

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

View File

@@ -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()),

View File

@@ -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)
});

View File

@@ -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.

View File

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

View File

@@ -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 {

View File

@@ -33,6 +33,7 @@ impl Default for Demos {
Box::<super::highlighting::Highlighting>::default(),
Box::<super::interactive_container::InteractiveContainerDemo>::default(),
Box::<super::MiscDemoWindow>::default(),
Box::<super::modals::Modals>::default(),
Box::<super::multi_touch::MultiTouch>::default(),
Box::<super::painting::Painting>::default(),
Box::<super::pan_zoom::PanZoom>::default(),
@@ -425,7 +426,7 @@ mod tests {
let result = harness.try_wgpu_snapshot_options(&format!("demos/{name}"), &options);
if let Err(err) = result {
errors.push(err);
errors.push(err.to_string());
}
}

View File

@@ -17,6 +17,7 @@ pub mod frame_demo;
pub mod highlighting;
pub mod interactive_container;
pub mod misc_demo_window;
pub mod modals;
pub mod multi_touch;
pub mod paint_bezier;
pub mod painting;

View File

@@ -0,0 +1,287 @@
use egui::{ComboBox, Context, Id, Modal, ProgressBar, Ui, Widget, Window};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct Modals {
user_modal_open: bool,
save_modal_open: bool,
save_progress: Option<f32>,
role: &'static str,
name: String,
}
impl Default for Modals {
fn default() -> Self {
Self {
user_modal_open: false,
save_modal_open: false,
save_progress: None,
role: Self::ROLES[0],
name: "John Doe".to_owned(),
}
}
}
impl Modals {
const ROLES: [&'static str; 2] = ["user", "admin"];
}
impl crate::Demo for Modals {
fn name(&self) -> &'static str {
"🗖 Modals"
}
fn show(&mut self, ctx: &Context, open: &mut bool) {
use crate::View as _;
Window::new(self.name())
.open(open)
.vscroll(false)
.resizable(false)
.show(ctx, |ui| self.ui(ui));
}
}
impl crate::View for Modals {
fn ui(&mut self, ui: &mut Ui) {
let Self {
user_modal_open,
save_modal_open,
save_progress,
role,
name,
} = self;
ui.horizontal(|ui| {
if ui.button("Open User Modal").clicked() {
*user_modal_open = true;
}
if ui.button("Open Save Modal").clicked() {
*save_modal_open = true;
}
});
ui.label("Click one of the buttons to open a modal.");
ui.label("Modals have a backdrop and prevent interaction with the rest of the UI.");
ui.label(
"You can show modals on top of each other and close the topmost modal with \
escape or by clicking outside the modal.",
);
if *user_modal_open {
let modal = Modal::new(Id::new("Modal A")).show(ui.ctx(), |ui| {
ui.set_width(250.0);
ui.heading("Edit User");
ui.label("Name:");
ui.text_edit_singleline(name);
ComboBox::new("role", "Role")
.selected_text(*role)
.show_ui(ui, |ui| {
for r in Self::ROLES {
ui.selectable_value(role, r, r);
}
});
ui.separator();
egui::Sides::new().show(
ui,
|_ui| {},
|ui| {
if ui.button("Save").clicked() {
*save_modal_open = true;
}
if ui.button("Cancel").clicked() {
*user_modal_open = false;
}
},
);
});
if modal.should_close() {
*user_modal_open = false;
}
}
if *save_modal_open {
let modal = Modal::new(Id::new("Modal B")).show(ui.ctx(), |ui| {
ui.set_width(200.0);
ui.heading("Save? Are you sure?");
ui.add_space(32.0);
egui::Sides::new().show(
ui,
|_ui| {},
|ui| {
if ui.button("Yes Please").clicked() {
*save_progress = Some(0.0);
}
if ui.button("No Thanks").clicked() {
*save_modal_open = false;
}
},
);
});
if modal.should_close() {
*save_modal_open = false;
}
}
if let Some(progress) = *save_progress {
Modal::new(Id::new("Modal C")).show(ui.ctx(), |ui| {
ui.set_width(70.0);
ui.heading("Saving…");
ProgressBar::new(progress).ui(ui);
if progress >= 1.0 {
*save_progress = None;
*save_modal_open = false;
*user_modal_open = false;
} else {
*save_progress = Some(progress + 0.003);
ui.ctx().request_repaint();
}
});
}
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
}
}
#[cfg(test)]
mod tests {
use crate::demo::modals::Modals;
use crate::Demo;
use egui::accesskit::Role;
use egui::Key;
use egui_kittest::kittest::Queryable;
use egui_kittest::Harness;
#[test]
fn clicking_escape_when_popup_open_should_not_close_modal() {
let initial_state = Modals {
user_modal_open: true,
..Modals::default()
};
let mut harness = Harness::new_state(
|ctx, modals| {
modals.show(ctx, &mut true);
},
initial_state,
);
harness.get_by_role(Role::ComboBox).click();
harness.run();
assert!(harness.ctx.memory(|mem| mem.any_popup_open()));
assert!(harness.state().user_modal_open);
harness.press_key(Key::Escape);
harness.run();
assert!(!harness.ctx.memory(|mem| mem.any_popup_open()));
assert!(harness.state().user_modal_open);
}
#[test]
fn escape_should_close_top_modal() {
let initial_state = Modals {
user_modal_open: true,
save_modal_open: true,
..Modals::default()
};
let mut harness = Harness::new_state(
|ctx, modals| {
modals.show(ctx, &mut true);
},
initial_state,
);
assert!(harness.state().user_modal_open);
assert!(harness.state().save_modal_open);
harness.press_key(Key::Escape);
harness.run();
assert!(harness.state().user_modal_open);
assert!(!harness.state().save_modal_open);
}
#[test]
fn should_match_snapshot() {
let initial_state = Modals {
user_modal_open: true,
..Modals::default()
};
let mut harness = Harness::new_state(
|ctx, modals| {
modals.show(ctx, &mut true);
},
initial_state,
);
let mut results = Vec::new();
harness.run();
results.push(harness.try_wgpu_snapshot("modals_1"));
harness.get_by_label("Save").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
results.push(harness.try_wgpu_snapshot("modals_2"));
harness.get_by_label("Yes Please").click();
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
results.push(harness.try_wgpu_snapshot("modals_3"));
for result in results {
result.unwrap();
}
}
// This tests whether the backdrop actually prevents interaction with lower layers.
#[test]
fn backdrop_should_prevent_focusing_lower_area() {
let initial_state = Modals {
save_modal_open: true,
save_progress: Some(0.0),
..Modals::default()
};
let mut harness = Harness::new_state(
|ctx, modals| {
modals.show(ctx, &mut true);
},
initial_state,
);
// TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests
harness.run();
harness.run();
harness.run();
harness.get_by_label("Yes Please").simulate_click();
harness.run();
// This snapshots should show the progress bar modal on top of the save modal.
harness.wgpu_snapshot("modals_backdrop_should_prevent_focusing_lower_area");
}
}

View File

@@ -73,20 +73,29 @@ impl crate::View for PanZoom {
for (i, (pos, callback)) in [
(
egui::Pos2::new(0.0, 0.0),
Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!"))
as Box<dyn Fn(&mut egui::Ui, &mut Self) -> egui::Response>,
Box::new(|ui: &mut egui::Ui, _: &mut Self| {
ui.button("top left").on_hover_text("Normal tooltip")
}) as Box<dyn Fn(&mut egui::Ui, &mut Self) -> egui::Response>,
),
(
egui::Pos2::new(0.0, 120.0),
Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")),
Box::new(|ui: &mut egui::Ui, _| {
ui.button("bottom left").on_hover_text("Normal tooltip")
}),
),
(
egui::Pos2::new(120.0, 120.0),
Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")),
Box::new(|ui: &mut egui::Ui, _| {
ui.button("right bottom")
.on_hover_text_at_pointer("Tooltip at pointer")
}),
),
(
egui::Pos2::new(120.0, 0.0),
Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")),
Box::new(|ui: &mut egui::Ui, _| {
ui.button("right top")
.on_hover_text_at_pointer("Tooltip at pointer")
}),
),
(
egui::Pos2::new(60.0, 60.0),

View File

@@ -1,5 +1,4 @@
use egui::{style::HandleShape, Slider, SliderClamping, SliderOrientation, Ui};
use std::f64::INFINITY;
/// Showcase sliders
#[derive(PartialEq)]
@@ -77,7 +76,7 @@ impl crate::View for Sliders {
let (type_min, type_max) = if *integer {
((i32::MIN as f64), (i32::MAX as f64))
} else if *logarithmic {
(-INFINITY, INFINITY)
(-f64::INFINITY, f64::INFINITY)
} else {
(-1e5, 1e5) // linear sliders make little sense with huge numbers
};

View File

@@ -13,7 +13,7 @@ pub enum Item<'a> {
// TODO(emilk): add Style here so empty heading still uses up the right amount of space.
Newline,
///
/// Text
Text(Style, &'a str),
/// title, url

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b8d4f004ee11ea68ae0f30657601b6e51403fcc3ca91fa5b8cdcb58585d8d40d
size 78318
oid sha256:01aaa4ef1a167a94fa1e5163550aabe4fa5e9f3a012b26170fe3088a6ca32d94
size 81064

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:026723cb5d89b32386a849328c34420ee9e3ae1f97cbf6fa3c4543141123549e
size 32890

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79ce1dbf7627579d4e10de6494e34d8fd9685506d7b35cb3c9148f90f8c01366
size 25144
oid sha256:ccfda16ef7cdf94f7fbbd2c0f8df6f6de7904969e2a66337920c32608a6f9f05
size 25357

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8348ff582e11fdc9baf008b5434f81f8d77b834479cb3765c87d1f4fd695e30f
size 48212

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:23482b77cbd817c66421a630e409ac3d8c5d24de00aa91e476e8d42b607c24b1
size 48104

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d94aa33d72c32f6f1aafab92c9753dc07bc5224c701003ac7fe8a01ae8c701a
size 44011

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e1a5d265470c36e64340ccceea4ade464b3c4a1177d60630b02ae8287934748f
size 44026

View File

@@ -28,7 +28,7 @@ pub struct RetainedImage {
}
impl RetainedImage {
pub fn from_color_image(debug_name: impl Into<String>, image: ColorImage) -> Self {
pub fn from_color_image(debug_name: impl Into<String>, image: egui::ColorImage) -> Self {
Self {
debug_name: debug_name.into(),
size: image.size,
@@ -54,7 +54,7 @@ impl RetainedImage {
) -> Result<Self, String> {
Ok(Self::from_color_image(
debug_name,
load_image_bytes(image_bytes)?,
load_image_bytes(image_bytes).map_err(|err| err.to_string())?,
))
}
@@ -154,7 +154,7 @@ impl RetainedImage {
self.texture
.lock()
.get_or_insert_with(|| {
let image: &mut ColorImage = &mut self.image.lock();
let image: &mut egui::ColorImage = &mut self.image.lock();
let image = std::mem::take(image);
ctx.load_texture(&self.debug_name, image, self.options)
})
@@ -190,8 +190,6 @@ impl RetainedImage {
// ----------------------------------------------------------------------------
use egui::ColorImage;
/// Load a (non-svg) image.
///
/// Requires the "image" feature. You must also opt-in to the image formats you need
@@ -200,9 +198,19 @@ use egui::ColorImage;
/// # Errors
/// On invalid image or unsupported image format.
#[cfg(feature = "image")]
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, String> {
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, egui::load::LoadError> {
crate::profile_function!();
let image = image::load_from_memory(image_bytes).map_err(|err| err.to_string())?;
let image = image::load_from_memory(image_bytes).map_err(|err| match err {
image::ImageError::Unsupported(err) => match err.kind() {
image::error::UnsupportedErrorKind::Format(format) => {
egui::load::LoadError::FormatNotSupported {
detected_format: Some(format.to_string()),
}
}
_ => egui::load::LoadError::Loading(err.to_string()),
},
err => egui::load::LoadError::Loading(err.to_string()),
})?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();

View File

@@ -7,7 +7,7 @@ use egui::{
use image::ImageFormat;
use std::{mem::size_of, path::Path, sync::Arc};
type Entry = Result<Arc<ColorImage>, String>;
type Entry = Result<Arc<ColorImage>, LoadError>;
#[derive(Default)]
pub struct ImageCrateLoader {
@@ -31,9 +31,14 @@ fn is_supported_uri(uri: &str) -> bool {
.any(|format_ext| ext == *format_ext)
}
fn is_unsupported_mime(mime: &str) -> bool {
fn is_supported_mime(mime: &str) -> bool {
// This is the default mime type for binary files, so this might actually be a valid image,
// let's relay on image's format guessing
if mime == "application/octet-stream" {
return true;
}
// Uses only the enabled image crate features
!ImageFormat::all()
ImageFormat::all()
.filter(ImageFormat::reading_enabled)
.map(|fmt| fmt.to_mime_type())
.any(|format_mime| mime == format_mime)
@@ -46,12 +51,12 @@ impl ImageLoader for ImageCrateLoader {
fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult {
// three stages of guessing if we support loading the image:
// 1. URI extension
// 1. URI extension (only done for files)
// 2. Mime from `BytesPoll::Ready`
// 3. image::guess_format
// 3. image::guess_format (used internally by image::load_from_memory)
// (1)
if !is_supported_uri(uri) {
if uri.starts_with("file://") && !is_supported_uri(uri) {
return Err(LoadError::NotSupported);
}
@@ -59,26 +64,26 @@ impl ImageLoader for ImageCrateLoader {
if let Some(entry) = cache.get(uri).cloned() {
match entry {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Loading(err)),
Err(err) => Err(err),
}
} else {
match ctx.try_load_bytes(uri) {
Ok(BytesPoll::Ready { bytes, mime, .. }) => {
// (2 and 3)
if mime.as_deref().is_some_and(is_unsupported_mime)
|| image::guess_format(&bytes).is_err()
{
return Err(LoadError::NotSupported);
// (2)
if let Some(mime) = mime {
if !is_supported_mime(&mime) {
return Err(LoadError::FormatNotSupported {
detected_format: Some(mime),
});
}
}
// (3)
log::trace!("started loading {uri:?}");
let result = crate::image::load_image_bytes(&bytes).map(Arc::new);
log::trace!("finished loading {uri:?}");
cache.insert(uri.into(), result.clone());
match result {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Loading(err)),
}
result.map(|image| ImagePoll::Ready { image })
}
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
Err(err) => Err(err),
@@ -100,7 +105,7 @@ impl ImageLoader for ImageCrateLoader {
.values()
.map(|result| match result {
Ok(image) => image.pixels.len() * size_of::<egui::Color32>(),
Err(err) => err.len(),
Err(err) => err.byte_size(),
})
.sum()
}

View File

@@ -33,9 +33,7 @@ pub fn highlight(
// performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available
// (ui.ctx(), ui.style()) can be used
impl egui::util::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob>
for Highlighter
{
impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter {
fn compute(
&mut self,
(font_id, theme, code, lang): (&egui::FontId, &CodeTheme, &str, &str),
@@ -44,7 +42,7 @@ pub fn highlight(
}
}
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
type HighlightCache = egui::cache::FrameCache<LayoutJob, Highlighter>;
let font_id = style
.override_font_id

View File

@@ -53,6 +53,9 @@ impl SnapshotOptions {
pub enum SnapshotError {
/// Image did not match snapshot
Diff {
/// Name of the test
name: String,
/// Count of pixels that were different
diff: i32,
@@ -72,6 +75,9 @@ pub enum SnapshotError {
/// The size of the image did not match the snapshot
SizeMismatch {
/// Name of the test
name: String,
/// Expected size
expected: (u32, u32),
@@ -89,32 +95,43 @@ pub enum SnapshotError {
},
}
const HOW_TO_UPDATE_SCREENSHOTS: &str =
"Run `UPDATE_SNAPSHOTS=1 cargo test` to update the snapshots.";
impl Display for SnapshotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Diff { diff, diff_path } => {
Self::Diff {
name,
diff,
diff_path,
} => {
write!(
f,
"Image did not match snapshot. Diff: {diff}, {diff_path:?}"
"'{name}' Image did not match snapshot. Diff: {diff}, {diff_path:?}. {HOW_TO_UPDATE_SCREENSHOTS}"
)
}
Self::OpenSnapshot { path, err } => match err {
ImageError::IoError(io) => match io.kind() {
ErrorKind::NotFound => {
write!(f, "Missing snapshot: {path:?}")
write!(f, "Missing snapshot: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}")
}
err => {
write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}")
write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}")
}
},
err => {
write!(f, "Error decoding snapshot: {err:?}\nAt: {path:?}")
write!(f, "Error decoding snapshot: {err:?}\nAt: {path:?}. Make sure git-lfs is setup correctly. Read the instructions here: https://github.com/emilk/egui/blob/master/CONTRIBUTING.md#making-a-pr")
}
},
Self::SizeMismatch { expected, actual } => {
Self::SizeMismatch {
name,
expected,
actual,
} => {
write!(
f,
"Image size did not match snapshot. Expected: {expected:?}, Actual: {actual:?}"
"'{name}' Image size did not match snapshot. Expected: {expected:?}, Actual: {actual:?}. {HOW_TO_UPDATE_SCREENSHOTS}"
)
}
Self::WriteSnapshot { path, err } => {
@@ -194,6 +211,7 @@ pub fn try_image_snapshot_options(
if previous.dimensions() != current.dimensions() {
maybe_update_snapshot(&path, current)?;
return Err(SnapshotError::SizeMismatch {
name: name.to_owned(),
expected: previous.dimensions(),
actual: current.dimensions(),
});
@@ -217,13 +235,16 @@ pub fn try_image_snapshot_options(
err,
})?;
maybe_update_snapshot(&path, current)?;
return Err(SnapshotError::Diff { diff, diff_path });
Err(SnapshotError::Diff {
name: name.to_owned(),
diff,
diff_path,
})
} else {
// Delete old diff if it exists
std::fs::remove_file(diff_path).ok();
Ok(())
}
Ok(())
}
/// Image snapshot test.

View File

@@ -18,8 +18,8 @@ macro_rules! impl_numeric_float {
($t: ident) => {
impl Numeric for $t {
const INTEGRAL: bool = false;
const MIN: Self = std::$t::MIN;
const MAX: Self = std::$t::MAX;
const MIN: Self = $t::MIN;
const MAX: Self = $t::MAX;
#[inline(always)]
fn to_f64(self) -> f64 {
@@ -44,8 +44,8 @@ macro_rules! impl_numeric_integer {
($t: ident) => {
impl Numeric for $t {
const INTEGRAL: bool = true;
const MIN: Self = std::$t::MIN;
const MAX: Self = std::$t::MAX;
const MIN: Self = $t::MIN;
const MAX: Self = $t::MAX;
#[inline(always)]
fn to_f64(self) -> f64 {

View File

@@ -1,4 +1,3 @@
use std::f32::INFINITY;
use std::fmt;
use crate::{lerp, pos2, vec2, Div, Mul, Pos2, Rangef, Rot2, Vec2};
@@ -33,8 +32,8 @@ pub struct Rect {
impl Rect {
/// Infinite rectangle that contains every point.
pub const EVERYTHING: Self = Self {
min: pos2(-INFINITY, -INFINITY),
max: pos2(INFINITY, INFINITY),
min: pos2(-f32::INFINITY, -f32::INFINITY),
max: pos2(f32::INFINITY, f32::INFINITY),
};
/// The inverse of [`Self::EVERYTHING`]: stretches from positive infinity to negative infinity.
@@ -53,8 +52,8 @@ impl Rect {
/// assert_eq!(rect, Rect::from_min_max(pos2(0.0, 1.0), pos2(2.0, 3.0)))
/// ```
pub const NOTHING: Self = Self {
min: pos2(INFINITY, INFINITY),
max: pos2(-INFINITY, -INFINITY),
min: pos2(f32::INFINITY, f32::INFINITY),
max: pos2(-f32::INFINITY, -f32::INFINITY),
};
/// An invalid [`Rect`] filled with [`f32::NAN`].
@@ -650,6 +649,8 @@ impl Rect {
///
/// A ray that starts inside the rect will return `true`.
pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool {
debug_assert!(d.is_normalized(), "expected normalized direction");
let mut tmin = -f32::INFINITY;
let mut tmax = f32::INFINITY;
@@ -671,6 +672,32 @@ impl Rect {
0.0 <= tmax && tmin <= tmax
}
/// Where does a ray from the center intersect the rectangle?
///
/// `d` is the direction of the ray and assumed to be normalized.
pub fn intersects_ray_from_center(&self, d: Vec2) -> Pos2 {
debug_assert!(d.is_normalized(), "expected normalized direction");
let mut tmin = f32::NEG_INFINITY;
let mut tmax = f32::INFINITY;
for i in 0..2 {
let inv_d = 1.0 / -d[i];
let mut t0 = (self.min[i] - self.center()[i]) * inv_d;
let mut t1 = (self.max[i] - self.center()[i]) * inv_d;
if inv_d < 0.0 {
std::mem::swap(&mut t0, &mut t1);
}
tmin = tmin.max(t0);
tmax = tmax.min(t1);
}
let t = tmax.min(tmin);
self.center() + t * -d
}
}
impl fmt::Debug for Rect {
@@ -793,4 +820,57 @@ mod tests {
println!("Leftward ray from right:");
assert!(rect.intersects_ray(pos2(4.0, 2.0), Vec2::LEFT));
}
#[test]
fn test_ray_from_center_intersection() {
let rect = Rect::from_min_max(pos2(1.0, 1.0), pos2(3.0, 3.0));
assert_eq!(
rect.intersects_ray_from_center(Vec2::RIGHT),
pos2(3.0, 2.0),
"rightward ray"
);
assert_eq!(
rect.intersects_ray_from_center(Vec2::UP),
pos2(2.0, 1.0),
"upward ray"
);
assert_eq!(
rect.intersects_ray_from_center(Vec2::LEFT),
pos2(1.0, 2.0),
"leftward ray"
);
assert_eq!(
rect.intersects_ray_from_center(Vec2::DOWN),
pos2(2.0, 3.0),
"downward ray"
);
assert_eq!(
rect.intersects_ray_from_center((Vec2::LEFT + Vec2::DOWN).normalized()),
pos2(1.0, 3.0),
"bottom-left corner ray"
);
assert_eq!(
rect.intersects_ray_from_center((Vec2::LEFT + Vec2::UP).normalized()),
pos2(1.0, 1.0),
"top-left corner ray"
);
assert_eq!(
rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::DOWN).normalized()),
pos2(3.0, 3.0),
"bottom-right corner ray"
);
assert_eq!(
rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::UP).normalized()),
pos2(3.0, 1.0),
"top-right corner ray"
);
}
}

View File

@@ -138,7 +138,9 @@ fn test_aim() {
assert_eq!(best_in_range_f64(99.999, 100.000), 100.0);
assert_eq!(best_in_range_f64(10.001, 100.001), 100.0);
use std::f64::{INFINITY, NAN, NEG_INFINITY};
const NAN: f64 = f64::NAN;
const INFINITY: f64 = f64::INFINITY;
const NEG_INFINITY: f64 = f64::NEG_INFINITY;
assert!(best_in_range_f64(NAN, NAN).is_nan());
assert_eq!(best_in_range_f64(NAN, 1.2), 1.2);
assert_eq!(best_in_range_f64(NAN, INFINITY), INFINITY);

View File

@@ -176,6 +176,12 @@ impl Vec2 {
}
}
/// Checks if `self` has length `1.0` up to a precision of `1e-6`.
#[inline(always)]
pub fn is_normalized(self) -> bool {
(self.length_sq() - 1.0).abs() < 2e-6
}
/// Rotates the vector by 90°, i.e positive X to positive Y
/// (clockwise in egui coordinates).
#[inline(always)]
@@ -497,8 +503,10 @@ impl fmt::Display for Vec2 {
}
}
#[test]
fn test_vec2() {
#[cfg(test)]
mod test {
use super::*;
macro_rules! almost_eq {
($left: expr, $right: expr) => {
let left = $left;
@@ -506,32 +514,58 @@ fn test_vec2() {
assert!((left - right).abs() < 1e-6, "{} != {}", left, right);
};
}
use std::f32::consts::TAU;
assert_eq!(Vec2::ZERO.angle(), 0.0);
assert_eq!(Vec2::angled(0.0).angle(), 0.0);
assert_eq!(Vec2::angled(1.0).angle(), 1.0);
assert_eq!(Vec2::X.angle(), 0.0);
assert_eq!(Vec2::Y.angle(), 0.25 * TAU);
#[test]
fn test_vec2() {
use std::f32::consts::TAU;
assert_eq!(Vec2::RIGHT.angle(), 0.0);
assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU);
almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU);
assert_eq!(Vec2::UP.angle(), -0.25 * TAU);
assert_eq!(Vec2::ZERO.angle(), 0.0);
assert_eq!(Vec2::angled(0.0).angle(), 0.0);
assert_eq!(Vec2::angled(1.0).angle(), 1.0);
assert_eq!(Vec2::X.angle(), 0.0);
assert_eq!(Vec2::Y.angle(), 0.25 * TAU);
let mut assignment = vec2(1.0, 2.0);
assignment += vec2(3.0, 4.0);
assert_eq!(assignment, vec2(4.0, 6.0));
assert_eq!(Vec2::RIGHT.angle(), 0.0);
assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU);
almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU);
assert_eq!(Vec2::UP.angle(), -0.25 * TAU);
let mut assignment = vec2(4.0, 6.0);
assignment -= vec2(1.0, 2.0);
assert_eq!(assignment, vec2(3.0, 4.0));
let mut assignment = vec2(1.0, 2.0);
assignment += vec2(3.0, 4.0);
assert_eq!(assignment, vec2(4.0, 6.0));
let mut assignment = vec2(1.0, 2.0);
assignment *= 2.0;
assert_eq!(assignment, vec2(2.0, 4.0));
let mut assignment = vec2(4.0, 6.0);
assignment -= vec2(1.0, 2.0);
assert_eq!(assignment, vec2(3.0, 4.0));
let mut assignment = vec2(2.0, 4.0);
assignment /= 2.0;
assert_eq!(assignment, vec2(1.0, 2.0));
let mut assignment = vec2(1.0, 2.0);
assignment *= 2.0;
assert_eq!(assignment, vec2(2.0, 4.0));
let mut assignment = vec2(2.0, 4.0);
assignment /= 2.0;
assert_eq!(assignment, vec2(1.0, 2.0));
}
#[test]
fn test_vec2_normalized() {
fn generate_spiral(n: usize, start: Vec2, end: Vec2) -> impl Iterator<Item = Vec2> {
let angle_step = 2.0 * std::f32::consts::PI / n as f32;
let radius_step = (end.length() - start.length()) / n as f32;
(0..n).map(move |i| {
let angle = i as f32 * angle_step;
let radius = start.length() + i as f32 * radius_step;
let x = radius * angle.cos();
let y = radius * angle.sin();
vec2(x, y)
})
}
for v in generate_spiral(40, Vec2::splat(0.1), Vec2::splat(2.0)) {
let vn = v.normalized();
almost_eq!(vn.length(), 1.0);
assert!(vn.is_normalized());
}
}
}

View File

@@ -75,7 +75,7 @@ mod mutex_impl {
// Detect if we are recursively taking out a lock on this mutex.
// use a pointer to the inner data as an id for this lock
let ptr = (&self.0 as *const parking_lot::Mutex<_>).cast::<()>();
let ptr = std::ptr::from_ref::<parking_lot::Mutex<_>>(&self.0).cast::<()>();
// Store it in thread local storage while we have a lock guard taken out
HELD_LOCKS_TLS.with(|held_locks| {

View File

@@ -648,10 +648,10 @@ pub struct Glyph {
/// The row/line height of this font.
pub font_height: f32,
/// The ascent of the sub-font within the font ("FontImpl").
/// The ascent of the sub-font within the font (`FontImpl`).
pub font_impl_ascent: f32,
/// The row/line height of the sub-font within the font ("FontImpl").
/// The row/line height of the sub-font within the font (`FontImpl`).
pub font_impl_height: f32,
/// Position and size of the glyph in the font texture, in texels.

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["tami5 <kkharji@proton.me>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Varphone Wong <varphone@qq.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -3,7 +3,7 @@ name = "custom_style"
version = "0.1.0"
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Maxim Osipenko <maxim1999max@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]
@@ -29,6 +29,4 @@ env_logger = { version = "0.10", default-features = false, features = [
] }
# This is normally enabled by eframe/default, which is not being used here
# because of accesskit, as mentioned above
winit = { workspace = true, features = [
"default"
] }
winit = { workspace = true, features = ["default"] }

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Jan Procházka <github.com/jprochazk>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Jose Palazon <jose@palako.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -7,7 +7,7 @@ authors = [
]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -45,7 +45,7 @@ impl eframe::App for MyApp {
if ui.button("save to 'top_left.png'").clicked() {
self.save_to_file = true;
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot);
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(Default::default()));
}
ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| {
@@ -58,9 +58,13 @@ impl eframe::App for MyApp {
} else {
ctx.set_theme(egui::Theme::Light);
};
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot);
ctx.send_viewport_cmd(
egui::ViewportCommand::Screenshot(Default::default()),
);
} else if ui.button("take screenshot!").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot);
ctx.send_viewport_cmd(
egui::ViewportCommand::Screenshot(Default::default()),
);
}
});
});

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["TicClick <ya@ticclick.ch>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -5,6 +5,6 @@
# to the user in the error, instead of "error: invalid channel name '[toolchain]'".
[toolchain]
channel = "1.77.0"
channel = "1.79.0"
components = ["rustfmt", "clippy"]
targets = ["wasm32-unknown-unknown"]

View File

@@ -9,7 +9,7 @@ set -x
# Checks all tests, lints etc.
# Basically does what the CI does.
cargo +1.77.0 install --quiet typos-cli
cargo +1.79.0 install --quiet typos-cli
export RUSTFLAGS="-D warnings"
export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454

View File

@@ -6,7 +6,7 @@
# -----------------------------------------------------------------------------
# Section identical to the root clippy.toml:
msrv = "1.77"
msrv = "1.79"
allow-unwrap-in-tests = true
@@ -47,6 +47,9 @@ doc-valid-idents = [
# You must also update the same list in the root `clippy.toml`!
"AccessKit",
"WebGL",
"WebGL1",
"WebGL2",
"WebGPU",
"VirtualBox",
"..",
]

View File

@@ -3,14 +3,17 @@ name = "test_egui_extras_compilation"
version = "0.1.0"
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]
workspace = true
[package.metadata.cargo-machete]
ignored = ["eframe", "egui_extras"] # We don't use them, just check that things compile
ignored = [
"eframe",
"egui_extras",
] # We don't use them, just check that things compile
[dependencies]
eframe = { workspace = true, features = ["default", "persistence"] }

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Antoine Beyeler <abeyeler@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["konkitoman"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.77"
rust-version = "1.79"
publish = false
[lints]