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:
2
.github/workflows/cargo_machete.yml
vendored
2
.github/workflows/cargo_machete.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/deploy_web_demo.yml
vendored
2
.github/workflows/deploy_web_demo.yml
vendored
@@ -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
|
||||
|
||||
14
.github/workflows/rust.yml
vendored
14
.github/workflows/rust.yml
vendored
@@ -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
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
/crates/egui_kittest @lucasmerlin
|
||||
/crates/egui-wgpu @Wumpf
|
||||
|
||||
16
Cargo.lock
16
Cargo.lock
@@ -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]]
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -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
|
||||
|
||||
@@ -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",
|
||||
"..",
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>>>,
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
287
crates/egui_demo_lib/src/demo/modals.rs
Normal file
287
crates/egui_demo_lib/src/demo/modals.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b8d4f004ee11ea68ae0f30657601b6e51403fcc3ca91fa5b8cdcb58585d8d40d
|
||||
size 78318
|
||||
oid sha256:01aaa4ef1a167a94fa1e5163550aabe4fa5e9f3a012b26170fe3088a6ca32d94
|
||||
size 81064
|
||||
|
||||
3
crates/egui_demo_lib/tests/snapshots/demos/Modals.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/demos/Modals.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:026723cb5d89b32386a849328c34420ee9e3ae1f97cbf6fa3c4543141123549e
|
||||
size 32890
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79ce1dbf7627579d4e10de6494e34d8fd9685506d7b35cb3c9148f90f8c01366
|
||||
size 25144
|
||||
oid sha256:ccfda16ef7cdf94f7fbbd2c0f8df6f6de7904969e2a66337920c32608a6f9f05
|
||||
size 25357
|
||||
|
||||
3
crates/egui_demo_lib/tests/snapshots/modals_1.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/modals_1.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8348ff582e11fdc9baf008b5434f81f8d77b834479cb3765c87d1f4fd695e30f
|
||||
size 48212
|
||||
3
crates/egui_demo_lib/tests/snapshots/modals_2.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/modals_2.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:23482b77cbd817c66421a630e409ac3d8c5d24de00aa91e476e8d42b607c24b1
|
||||
size 48104
|
||||
3
crates/egui_demo_lib/tests/snapshots/modals_3.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/modals_3.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d94aa33d72c32f6f1aafab92c9753dc07bc5224c701003ac7fe8a01ae8c701a
|
||||
size 44011
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e1a5d265470c36e64340ccceea4ade464b3c4a1177d60630b02ae8287934748f
|
||||
size 44026
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -7,7 +7,7 @@ authors = [
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
rust-version = "1.79"
|
||||
publish = false
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -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()),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
"..",
|
||||
]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user