mirror of
https://github.com/emilk/egui.git
synced 2026-06-27 15:13:12 -04:00
Merge branch 'master' of https://github.com/emilk/egui into multiples_viewports
This commit is contained in:
10
.github/workflows/rust.yml
vendored
10
.github/workflows/rust.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.67.0
|
||||
toolchain: 1.70.0
|
||||
|
||||
- name: Install packages (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.67.0
|
||||
toolchain: 1.70.0
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
rust-version: "1.67.0"
|
||||
rust-version: "1.70.0"
|
||||
log-level: error
|
||||
command: check
|
||||
arguments: --target ${{ matrix.target }}
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.67.0
|
||||
toolchain: 1.70.0
|
||||
targets: aarch64-linux-android
|
||||
|
||||
- name: Set up cargo cache
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: 1.67.0
|
||||
toolchain: 1.70.0
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -1089,10 +1089,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
"ehttp",
|
||||
"env_logger",
|
||||
"image",
|
||||
"poll-promise",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1264,6 +1262,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"document-features",
|
||||
"egui",
|
||||
"ehttp",
|
||||
"image",
|
||||
"log",
|
||||
"puffin",
|
||||
@@ -1303,10 +1302,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ehttp"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80b69a6f9168b96c0ae04763bec27a8b06b34343c334dd2703a4ec21f0f5e110"
|
||||
checksum = "31e4525e883dd283d12b755ab3ad71d7c8dea2ee8e8a062b9f4c4f84637ed681"
|
||||
dependencies = [
|
||||
"document-features",
|
||||
"js-sys",
|
||||
"ureq",
|
||||
"wasm-bindgen",
|
||||
@@ -3167,7 +3167,6 @@ dependencies = [
|
||||
"eframe",
|
||||
"env_logger",
|
||||
"image",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
37
Cranky.toml
37
Cranky.toml
@@ -1,20 +1,30 @@
|
||||
# https://github.com/ericseppanen/cargo-cranky
|
||||
# cargo install cargo-cranky && cargo cranky
|
||||
# See also clippy.toml
|
||||
|
||||
deny = ["unsafe_code"]
|
||||
deny = [
|
||||
"unsafe_code",
|
||||
# Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
|
||||
#"clippy::self_named_module_files",
|
||||
]
|
||||
|
||||
warn = [
|
||||
"clippy::all",
|
||||
"clippy::as_ptr_cast_mut",
|
||||
"clippy::await_holding_lock",
|
||||
"clippy::bool_to_int_with_if",
|
||||
"clippy::char_lit_as_u8",
|
||||
"clippy::checked_conversions",
|
||||
"clippy::clear_with_drain",
|
||||
"clippy::cloned_instead_of_copied",
|
||||
"clippy::dbg_macro",
|
||||
"clippy::debug_assert_with_mut_call",
|
||||
"clippy::derive_partial_eq_without_eq",
|
||||
"clippy::disallowed_methods",
|
||||
"clippy::disallowed_script_idents",
|
||||
"clippy::disallowed_macros", # See clippy.toml
|
||||
"clippy::disallowed_methods", # See clippy.toml
|
||||
"clippy::disallowed_names", # See clippy.toml
|
||||
"clippy::disallowed_script_idents", # See clippy.toml
|
||||
"clippy::disallowed_types", # See clippy.toml
|
||||
"clippy::doc_link_with_quotes",
|
||||
"clippy::doc_markdown",
|
||||
"clippy::empty_enum",
|
||||
@@ -32,6 +42,7 @@ warn = [
|
||||
"clippy::fn_params_excessive_bools",
|
||||
"clippy::fn_to_numeric_cast_any",
|
||||
"clippy::from_iter_instead_of_collect",
|
||||
"clippy::get_unwrap",
|
||||
"clippy::if_let_mutex",
|
||||
"clippy::implicit_clone",
|
||||
"clippy::imprecise_flops",
|
||||
@@ -42,6 +53,7 @@ warn = [
|
||||
"clippy::iter_on_empty_collections",
|
||||
"clippy::iter_on_single_items",
|
||||
"clippy::large_digit_groups",
|
||||
"clippy::large_include_file",
|
||||
"clippy::large_stack_arrays",
|
||||
"clippy::large_types_passed_by_value",
|
||||
"clippy::let_unit_value",
|
||||
@@ -49,7 +61,9 @@ warn = [
|
||||
"clippy::lossy_float_literal",
|
||||
"clippy::macro_use_imports",
|
||||
"clippy::manual_assert",
|
||||
"clippy::manual_clamp",
|
||||
"clippy::manual_instant_elapsed",
|
||||
"clippy::manual_let_else",
|
||||
"clippy::manual_ok_or",
|
||||
"clippy::manual_string_new",
|
||||
"clippy::map_err_ignore",
|
||||
@@ -81,17 +95,24 @@ warn = [
|
||||
"clippy::rest_pat_in_fully_bound_structs",
|
||||
"clippy::same_functions_in_if_condition",
|
||||
"clippy::semicolon_if_nothing_returned",
|
||||
"clippy::significant_drop_tightening",
|
||||
"clippy::single_match_else",
|
||||
"clippy::str_to_string",
|
||||
"clippy::string_add_assign",
|
||||
"clippy::string_add",
|
||||
"clippy::string_lit_as_bytes",
|
||||
"clippy::string_to_string",
|
||||
"clippy::suspicious_command_arg_space",
|
||||
"clippy::suspicious_xor_used_as_pow",
|
||||
"clippy::todo",
|
||||
"clippy::trailing_empty_array",
|
||||
"clippy::trait_duplication_in_bounds",
|
||||
"clippy::unchecked_duration_subtraction",
|
||||
"clippy::unimplemented",
|
||||
"clippy::uninlined_format_args",
|
||||
"clippy::unnecessary_box_returns",
|
||||
"clippy::unnecessary_safety_doc",
|
||||
"clippy::unnecessary_struct_initialization",
|
||||
"clippy::unnecessary_wraps",
|
||||
"clippy::unnested_or_patterns",
|
||||
"clippy::unused_peekable",
|
||||
@@ -99,6 +120,7 @@ warn = [
|
||||
"clippy::unused_self",
|
||||
"clippy::useless_transmute",
|
||||
"clippy::verbose_file_reads",
|
||||
"clippy::wildcard_dependencies",
|
||||
"clippy::zero_sized_map_values",
|
||||
"elided_lifetimes_in_paths",
|
||||
"future_incompatible",
|
||||
@@ -114,11 +136,14 @@ warn = [
|
||||
]
|
||||
|
||||
allow = [
|
||||
"clippy::manual_range_contains", # This one is just annoying
|
||||
"clippy::manual_range_contains", # this one is just worse imho
|
||||
|
||||
# Some of these we should try to put in "warn":
|
||||
"clippy::type_complexity",
|
||||
# TODO(emilk): enable more of these lints:
|
||||
"clippy::let_underscore_untyped",
|
||||
"clippy::missing_assert_message",
|
||||
"clippy::undocumented_unsafe_blocks",
|
||||
"clippy::unwrap_used",
|
||||
"clippy::wildcard_imports",
|
||||
"trivial_casts",
|
||||
"unsafe_op_in_unsafe_fn", # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
|
||||
"unused_qualifications",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Section identical to scripts/clippy_wasm/clippy.toml:
|
||||
|
||||
msrv = "1.67"
|
||||
msrv = "1.70"
|
||||
|
||||
allow-unwrap-in-tests = true
|
||||
|
||||
@@ -14,9 +14,10 @@ avoid-breaking-exported-api = false
|
||||
max-fn-params-bools = 2 # TODO(emilk): decrease this to 1
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/large_include_file
|
||||
max-include-file-size = 100000
|
||||
max-include-file-size = 1000000
|
||||
|
||||
too-many-lines-threshold = 100
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/type_complexity
|
||||
type-complexity-threshold = 350
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ authors = [
|
||||
]
|
||||
description = "Color structs and color conversion utilities"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -38,7 +38,7 @@ pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)]
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
impl std::hash::Hash for Rgba {
|
||||
#[inline]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.22.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "egui framework - write GUI apps that compiles to web and/or natively"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -35,7 +35,6 @@ pub fn pos_from_touch_event(
|
||||
// search for the touch we previously used for the position
|
||||
// (unfortunately, `event.touches()` is not a rust collection):
|
||||
(0..event.touches().length())
|
||||
.into_iter()
|
||||
.map(|i| event.touches().get(i).unwrap())
|
||||
.find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos)
|
||||
} else {
|
||||
|
||||
@@ -148,9 +148,7 @@ impl WebPainter for WebPainterWgpu {
|
||||
) -> Result<(), JsValue> {
|
||||
let size_in_pixels = [self.canvas.width(), self.canvas.height()];
|
||||
|
||||
let render_state = if let Some(render_state) = &self.render_state {
|
||||
render_state
|
||||
} else {
|
||||
let Some(render_state) = &self.render_state else {
|
||||
return Err(JsValue::from_str(
|
||||
"Can't paint, wgpu renderer was already disposed",
|
||||
));
|
||||
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
|
||||
]
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -435,9 +435,7 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
Primitive::Callback(callback) => {
|
||||
let cbfn = if let Some(c) = callback.callback.downcast_ref::<Callback>() {
|
||||
c
|
||||
} else {
|
||||
let Some(cbfn) = callback.callback.downcast_ref::<Callback>() else {
|
||||
// We already warned in the `prepare` callback
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -181,12 +181,6 @@ impl Painter {
|
||||
/// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a
|
||||
/// valid [`winit::window::Window`].
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The raw Window handle associated with the given `window` must be a valid object to create a
|
||||
/// surface upon and must remain valid for the lifetime of the created surface. (The surface may
|
||||
/// be cleared by passing `None`).
|
||||
///
|
||||
/// # Errors
|
||||
/// If the provided wgpu configuration does not match an available device.
|
||||
pub async fn set_window(
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.22.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Bindings for using egui with winit"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -26,10 +26,6 @@ pub struct Clipboard {
|
||||
|
||||
impl Clipboard {
|
||||
/// Construct a new instance
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The returned `Clipboard` must not outlive the input `_display_target`.
|
||||
pub fn new(_display_target: &dyn HasRawDisplayHandle) -> Self {
|
||||
Self {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
|
||||
@@ -87,10 +87,6 @@ pub struct State {
|
||||
|
||||
impl State {
|
||||
/// Construct a new instance
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The returned `State` must not outlive the input `display_target`.
|
||||
pub fn new(display_target: &dyn HasRawDisplayHandle) -> Self {
|
||||
let egui_input = egui::RawInput {
|
||||
focused: false, // winit will tell us when we have focus
|
||||
|
||||
@@ -127,12 +127,10 @@ fn clamp_pos_to_monitors<E>(
|
||||
let monitors = event_loop.available_monitors();
|
||||
|
||||
// default to primary monitor, in case the correct monitor was disconnected.
|
||||
let mut active_monitor = if let Some(active_monitor) = event_loop
|
||||
let Some(mut active_monitor) = event_loop
|
||||
.primary_monitor()
|
||||
.or_else(|| event_loop.available_monitors().next())
|
||||
{
|
||||
active_monitor
|
||||
} else {
|
||||
else {
|
||||
return; // no monitors 🤷
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.22.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "An easy-to-use immediate mode GUI that runs on both web and native"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "../../README.md"
|
||||
|
||||
@@ -239,6 +239,8 @@ struct ContextImpl {
|
||||
is_accesskit_enabled: bool,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_node_classes: accesskit::NodeClassSet,
|
||||
|
||||
loaders: load::Loaders,
|
||||
}
|
||||
|
||||
impl ContextImpl {
|
||||
@@ -2294,6 +2296,159 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Image loading
|
||||
impl Context {
|
||||
/// Associate some static bytes with a `uri`.
|
||||
///
|
||||
/// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image.
|
||||
pub fn include_static_bytes(&self, uri: &'static str, bytes: &'static [u8]) {
|
||||
self.read(|ctx| ctx.loaders.include.insert_static(uri, bytes));
|
||||
}
|
||||
|
||||
/// Associate some bytes with a `uri`.
|
||||
///
|
||||
/// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image.
|
||||
pub fn include_bytes(&self, uri: &'static str, bytes: impl Into<Arc<[u8]>>) {
|
||||
self.read(|ctx| ctx.loaders.include.insert_shared(uri, bytes));
|
||||
}
|
||||
|
||||
/// Append an entry onto the chain of bytes loaders.
|
||||
///
|
||||
/// See [`load`] for more information.
|
||||
pub fn add_bytes_loader(&self, loader: Arc<dyn load::BytesLoader + Send + Sync + 'static>) {
|
||||
self.write(|ctx| ctx.loaders.bytes.push(loader));
|
||||
}
|
||||
|
||||
/// Append an entry onto the chain of image loaders.
|
||||
///
|
||||
/// See [`load`] for more information.
|
||||
pub fn add_image_loader(&self, loader: Arc<dyn load::ImageLoader + Send + Sync + 'static>) {
|
||||
self.write(|ctx| ctx.loaders.image.push(loader));
|
||||
}
|
||||
|
||||
/// Append an entry onto the chain of texture loaders.
|
||||
///
|
||||
/// See [`load`] for more information.
|
||||
pub fn add_texture_loader(&self, loader: Arc<dyn load::TextureLoader + Send + Sync + 'static>) {
|
||||
self.write(|ctx| ctx.loaders.texture.push(loader));
|
||||
}
|
||||
|
||||
/// Release all memory and textures related to the given image URI.
|
||||
///
|
||||
/// If you attempt to load the image again, it will be reloaded from scratch.
|
||||
pub fn forget_image(&self, uri: &str) {
|
||||
self.write(|ctx| {
|
||||
use crate::load::BytesLoader as _;
|
||||
|
||||
ctx.loaders.include.forget(uri);
|
||||
|
||||
for loader in &ctx.loaders.bytes {
|
||||
loader.forget(uri);
|
||||
}
|
||||
|
||||
for loader in &ctx.loaders.image {
|
||||
loader.forget(uri);
|
||||
}
|
||||
|
||||
for loader in &ctx.loaders.texture {
|
||||
loader.forget(uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Try loading the bytes from the given uri using any available bytes loaders.
|
||||
///
|
||||
/// Loaders are expected to cache results, so that this call is immediate-mode safe.
|
||||
///
|
||||
/// This calls the loaders one by one in the order in which they were registered.
|
||||
/// If a loader returns [`LoadError::NotSupported`][not_supported],
|
||||
/// then the next loader is called. This process repeats until all loaders have
|
||||
/// been exhausted, at which point this returns [`LoadError::NotSupported`][not_supported].
|
||||
///
|
||||
/// # Errors
|
||||
/// This may fail with:
|
||||
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
|
||||
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
|
||||
///
|
||||
/// [not_supported]: crate::load::LoadError::NotSupported
|
||||
/// [custom]: crate::load::LoadError::Custom
|
||||
pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult {
|
||||
self.read(|this| {
|
||||
for loader in &this.loaders.bytes {
|
||||
match loader.load(self, uri) {
|
||||
Err(load::LoadError::NotSupported) => continue,
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
|
||||
Err(load::LoadError::NotSupported)
|
||||
})
|
||||
}
|
||||
|
||||
/// Try loading the image from the given uri using any available image loaders.
|
||||
///
|
||||
/// Loaders are expected to cache results, so that this call is immediate-mode safe.
|
||||
///
|
||||
/// This calls the loaders one by one in the order in which they were registered.
|
||||
/// If a loader returns [`LoadError::NotSupported`][not_supported],
|
||||
/// then the next loader is called. This process repeats until all loaders have
|
||||
/// been exhausted, at which point this returns [`LoadError::NotSupported`][not_supported].
|
||||
///
|
||||
/// # Errors
|
||||
/// This may fail with:
|
||||
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
|
||||
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
|
||||
///
|
||||
/// [not_supported]: crate::load::LoadError::NotSupported
|
||||
/// [custom]: crate::load::LoadError::Custom
|
||||
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
|
||||
self.read(|this| {
|
||||
for loader in &this.loaders.image {
|
||||
match loader.load(self, uri, size_hint) {
|
||||
Err(load::LoadError::NotSupported) => continue,
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
|
||||
Err(load::LoadError::NotSupported)
|
||||
})
|
||||
}
|
||||
|
||||
/// Try loading the texture from the given uri using any available texture loaders.
|
||||
///
|
||||
/// Loaders are expected to cache results, so that this call is immediate-mode safe.
|
||||
///
|
||||
/// This calls the loaders one by one in the order in which they were registered.
|
||||
/// If a loader returns [`LoadError::NotSupported`][not_supported],
|
||||
/// then the next loader is called. This process repeats until all loaders have
|
||||
/// been exhausted, at which point this returns [`LoadError::NotSupported`][not_supported].
|
||||
///
|
||||
/// # Errors
|
||||
/// This may fail with:
|
||||
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
|
||||
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
|
||||
///
|
||||
/// [not_supported]: crate::load::LoadError::NotSupported
|
||||
/// [custom]: crate::load::LoadError::Custom
|
||||
pub fn try_load_texture(
|
||||
&self,
|
||||
uri: &str,
|
||||
texture_options: TextureOptions,
|
||||
size_hint: load::SizeHint,
|
||||
) -> load::TextureLoadResult {
|
||||
self.read(|this| {
|
||||
for loader in &this.loaders.texture {
|
||||
match loader.load(self, uri, texture_options, size_hint) {
|
||||
Err(load::LoadError::NotSupported) => continue,
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
|
||||
Err(load::LoadError::NotSupported)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Viewports
|
||||
impl Context {
|
||||
/// Return the `ViewportId` of the current viewport
|
||||
|
||||
@@ -314,6 +314,7 @@ mod input_state;
|
||||
pub mod introspection;
|
||||
pub mod layers;
|
||||
mod layout;
|
||||
pub mod load;
|
||||
mod memory;
|
||||
pub mod menu;
|
||||
pub mod os;
|
||||
@@ -371,6 +372,7 @@ pub use {
|
||||
input_state::{InputState, MultiTouchInfo, PointerState},
|
||||
layers::{LayerId, Order},
|
||||
layout::*,
|
||||
load::SizeHint,
|
||||
memory::{Memory, Options},
|
||||
painter::Painter,
|
||||
response::{InnerResponse, Response},
|
||||
|
||||
417
crates/egui/src/load.rs
Normal file
417
crates/egui/src/load.rs
Normal file
@@ -0,0 +1,417 @@
|
||||
//! Types and traits related to image loading.
|
||||
//!
|
||||
//! If you just want to load some images, see [`egui_extras`](https://crates.io/crates/egui_extras/),
|
||||
//! which contains reasonable default implementations of these traits. You can get started quickly
|
||||
//! using [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
|
||||
//!
|
||||
//! ## Loading process
|
||||
//!
|
||||
//! There are three kinds of loaders:
|
||||
//! - [`BytesLoader`]: load the raw bytes of an image
|
||||
//! - [`ImageLoader`]: decode the bytes into an array of colors
|
||||
//! - [`TextureLoader`]: ask the backend to put an image onto the GPU
|
||||
//!
|
||||
//! The different kinds of loaders represent different layers in the loading process:
|
||||
//!
|
||||
//! ```text,ignore
|
||||
//! ui.image2("file://image.png")
|
||||
//! └► ctx.try_load_texture("file://image.png", ...)
|
||||
//! └► TextureLoader::load("file://image.png", ...)
|
||||
//! └► ctx.try_load_image("file://image.png", ...)
|
||||
//! └► ImageLoader::load("file://image.png", ...)
|
||||
//! └► ctx.try_load_bytes("file://image.png", ...)
|
||||
//! └► BytesLoader::load("file://image.png", ...)
|
||||
//! ```
|
||||
//!
|
||||
//! As each layer attempts to load the URI, it first asks the layer below it
|
||||
//! for the data it needs to do its job. But this is not a strict requirement,
|
||||
//! an implementation could instead generate the data it needs!
|
||||
//!
|
||||
//! Loader trait implementations may be registered on a context with:
|
||||
//! - [`Context::add_bytes_loader`]
|
||||
//! - [`Context::add_image_loader`]
|
||||
//! - [`Context::add_texture_loader`]
|
||||
//!
|
||||
//! There may be multiple loaders of the same kind registered at the same time.
|
||||
//! The `try_load` methods on [`Context`] will attempt to call each loader one by one,
|
||||
//! until one of them returns something other than [`LoadError::NotSupported`].
|
||||
//!
|
||||
//! The loaders are stored in the context. This means they may hold state across frames,
|
||||
//! which they can (and _should_) use to cache the results of the operations they perform.
|
||||
//!
|
||||
//! For example, a [`BytesLoader`] that loads file URIs (`file://image.png`)
|
||||
//! would cache each file read. A [`TextureLoader`] would cache each combination
|
||||
//! of `(URI, TextureOptions)`, and so on.
|
||||
//!
|
||||
//! Each URI will be passed through the loaders as a plain `&str`.
|
||||
//! The loaders are free to derive as much meaning from the URI as they wish to.
|
||||
//! For example, a loader may determine that it doesn't support loading a specific URI
|
||||
//! if the protocol does not match what it expects.
|
||||
|
||||
use crate::Context;
|
||||
use ahash::HashMap;
|
||||
use epaint::mutex::Mutex;
|
||||
use epaint::TextureHandle;
|
||||
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
|
||||
use std::ops::Deref;
|
||||
use std::{error::Error as StdError, fmt::Display, sync::Arc};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum LoadError {
|
||||
/// This loader does not support this protocol or image format.
|
||||
NotSupported,
|
||||
|
||||
/// A custom error message (e.g. "File not found: foo.png").
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl Display for LoadError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LoadError::NotSupported => f.write_str("not supported"),
|
||||
LoadError::Custom(message) => f.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for LoadError {}
|
||||
|
||||
pub type Result<T, E = LoadError> = std::result::Result<T, E>;
|
||||
|
||||
/// Given as a hint for image loading requests.
|
||||
///
|
||||
/// Used mostly for rendering SVG:s to a good size.
|
||||
///
|
||||
/// All variants will preserve the original aspect ratio.
|
||||
///
|
||||
/// Similar to `usvg::FitTo`.
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum SizeHint {
|
||||
/// Keep original size.
|
||||
#[default]
|
||||
Original,
|
||||
|
||||
/// Scale to width.
|
||||
Width(u32),
|
||||
|
||||
/// Scale to height.
|
||||
Height(u32),
|
||||
|
||||
/// Scale to size.
|
||||
Size(u32, u32),
|
||||
}
|
||||
|
||||
impl From<Vec2> for SizeHint {
|
||||
fn from(value: Vec2) -> Self {
|
||||
Self::Size(value.x.round() as u32, value.y.round() as u32)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: API for querying bytes caches in each loader
|
||||
|
||||
pub type Size = [usize; 2];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Bytes {
|
||||
Static(&'static [u8]),
|
||||
Shared(Arc<[u8]>),
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for Bytes {
|
||||
#[inline]
|
||||
fn from(value: &'static [u8]) -> Self {
|
||||
Bytes::Static(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<[u8]>> for Bytes {
|
||||
#[inline]
|
||||
fn from(value: Arc<[u8]>) -> Self {
|
||||
Bytes::Shared(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Bytes {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
Bytes::Static(bytes) => bytes,
|
||||
Bytes::Shared(bytes) => bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Bytes {
|
||||
type Target = [u8];
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum BytesPoll {
|
||||
/// Bytes are being loaded.
|
||||
Pending {
|
||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||
size: Option<Size>,
|
||||
},
|
||||
|
||||
/// Bytes are loaded.
|
||||
Ready {
|
||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||
size: Option<Size>,
|
||||
|
||||
/// File contents, e.g. the contents of a `.png`.
|
||||
bytes: Bytes,
|
||||
},
|
||||
}
|
||||
|
||||
pub type BytesLoadResult = Result<BytesPoll>;
|
||||
|
||||
pub trait BytesLoader {
|
||||
/// Try loading the bytes from the given uri.
|
||||
///
|
||||
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
||||
/// once the data is ready.
|
||||
///
|
||||
/// The implementation should cache any result, so that calling this
|
||||
/// is immediate-mode safe.
|
||||
///
|
||||
/// # Errors
|
||||
/// This may fail with:
|
||||
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
|
||||
/// - [`LoadError::Custom`] if the loading process failed.
|
||||
fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult;
|
||||
|
||||
/// Forget the given `uri`.
|
||||
///
|
||||
/// If `uri` is cached, it should be evicted from cache,
|
||||
/// so that it may be fully reloaded.
|
||||
fn forget(&self, uri: &str);
|
||||
|
||||
/// Implementations may use this to perform work at the end of a frame,
|
||||
/// such as evicting unused entries from a cache.
|
||||
fn end_frame(&self, frame_index: usize) {
|
||||
let _ = frame_index;
|
||||
}
|
||||
|
||||
/// If the loader caches any data, this should return the size of that cache.
|
||||
fn byte_size(&self) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ImagePoll {
|
||||
/// Image is loading.
|
||||
Pending {
|
||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||
size: Option<Size>,
|
||||
},
|
||||
|
||||
/// Image is loaded.
|
||||
Ready { image: Arc<ColorImage> },
|
||||
}
|
||||
|
||||
pub type ImageLoadResult = Result<ImagePoll>;
|
||||
|
||||
pub trait ImageLoader {
|
||||
/// Try loading the image from the given uri.
|
||||
///
|
||||
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
||||
/// once the image is ready.
|
||||
///
|
||||
/// The implementation should cache any result, so that calling this
|
||||
/// is immediate-mode safe.
|
||||
///
|
||||
/// # Errors
|
||||
/// This may fail with:
|
||||
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
|
||||
/// - [`LoadError::Custom`] if the loading process failed.
|
||||
fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult;
|
||||
|
||||
/// Forget the given `uri`.
|
||||
///
|
||||
/// If `uri` is cached, it should be evicted from cache,
|
||||
/// so that it may be fully reloaded.
|
||||
fn forget(&self, uri: &str);
|
||||
|
||||
/// Implementations may use this to perform work at the end of a frame,
|
||||
/// such as evicting unused entries from a cache.
|
||||
fn end_frame(&self, frame_index: usize) {
|
||||
let _ = frame_index;
|
||||
}
|
||||
|
||||
/// If the loader caches any data, this should return the size of that cache.
|
||||
fn byte_size(&self) -> usize;
|
||||
}
|
||||
|
||||
/// A texture with a known size.
|
||||
#[derive(Clone)]
|
||||
pub struct SizedTexture {
|
||||
pub id: TextureId,
|
||||
pub size: Size,
|
||||
}
|
||||
|
||||
impl SizedTexture {
|
||||
pub fn from_handle(handle: &TextureHandle) -> Self {
|
||||
Self {
|
||||
id: handle.id(),
|
||||
size: handle.size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum TexturePoll {
|
||||
/// Texture is loading.
|
||||
Pending {
|
||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||
size: Option<Size>,
|
||||
},
|
||||
|
||||
/// Texture is loaded.
|
||||
Ready { texture: SizedTexture },
|
||||
}
|
||||
|
||||
pub type TextureLoadResult = Result<TexturePoll>;
|
||||
|
||||
pub trait TextureLoader {
|
||||
/// Try loading the texture from the given uri.
|
||||
///
|
||||
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
||||
/// once the texture is ready.
|
||||
///
|
||||
/// The implementation should cache any result, so that calling this
|
||||
/// is immediate-mode safe.
|
||||
///
|
||||
/// # Errors
|
||||
/// This may fail with:
|
||||
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
|
||||
/// - [`LoadError::Custom`] if the loading process failed.
|
||||
fn load(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
uri: &str,
|
||||
texture_options: TextureOptions,
|
||||
size_hint: SizeHint,
|
||||
) -> TextureLoadResult;
|
||||
|
||||
/// Forget the given `uri`.
|
||||
///
|
||||
/// If `uri` is cached, it should be evicted from cache,
|
||||
/// so that it may be fully reloaded.
|
||||
fn forget(&self, uri: &str);
|
||||
|
||||
/// Implementations may use this to perform work at the end of a frame,
|
||||
/// such as evicting unused entries from a cache.
|
||||
fn end_frame(&self, frame_index: usize) {
|
||||
let _ = frame_index;
|
||||
}
|
||||
|
||||
/// If the loader caches any data, this should return the size of that cache.
|
||||
fn byte_size(&self) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DefaultBytesLoader {
|
||||
cache: Mutex<HashMap<&'static str, Bytes>>,
|
||||
}
|
||||
|
||||
impl DefaultBytesLoader {
|
||||
pub(crate) fn insert_static(&self, uri: &'static str, bytes: &'static [u8]) {
|
||||
self.cache
|
||||
.lock()
|
||||
.entry(uri)
|
||||
.or_insert_with(|| Bytes::Static(bytes));
|
||||
}
|
||||
|
||||
pub(crate) fn insert_shared(&self, uri: &'static str, bytes: impl Into<Arc<[u8]>>) {
|
||||
self.cache
|
||||
.lock()
|
||||
.entry(uri)
|
||||
.or_insert_with(|| Bytes::Shared(bytes.into()));
|
||||
}
|
||||
}
|
||||
|
||||
impl BytesLoader for DefaultBytesLoader {
|
||||
fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
|
||||
match self.cache.lock().get(uri).cloned() {
|
||||
Some(bytes) => Ok(BytesPoll::Ready { size: None, bytes }),
|
||||
None => Err(LoadError::NotSupported),
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache.lock().values().map(|bytes| bytes.len()).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DefaultTextureLoader {
|
||||
cache: Mutex<HashMap<(String, TextureOptions), TextureHandle>>,
|
||||
}
|
||||
|
||||
impl TextureLoader for DefaultTextureLoader {
|
||||
fn load(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
uri: &str,
|
||||
texture_options: TextureOptions,
|
||||
size_hint: SizeHint,
|
||||
) -> TextureLoadResult {
|
||||
let mut cache = self.cache.lock();
|
||||
if let Some(handle) = cache.get(&(uri.into(), texture_options)) {
|
||||
let texture = SizedTexture::from_handle(handle);
|
||||
Ok(TexturePoll::Ready { texture })
|
||||
} else {
|
||||
match ctx.try_load_image(uri, size_hint)? {
|
||||
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
|
||||
ImagePoll::Ready { image } => {
|
||||
let handle = ctx.load_texture(uri, image, texture_options);
|
||||
let texture = SizedTexture::from_handle(&handle);
|
||||
cache.insert((uri.into(), texture_options), handle);
|
||||
Ok(TexturePoll::Ready { texture })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
self.cache.lock().retain(|(u, _), _| u != uri);
|
||||
}
|
||||
|
||||
fn end_frame(&self, _: usize) {}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|texture| texture.byte_size())
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Loaders {
|
||||
pub include: Arc<DefaultBytesLoader>,
|
||||
pub bytes: Vec<Arc<dyn BytesLoader + Send + Sync + 'static>>,
|
||||
pub image: Vec<Arc<dyn ImageLoader + Send + Sync + 'static>>,
|
||||
pub texture: Vec<Arc<dyn TextureLoader + Send + Sync + 'static>>,
|
||||
}
|
||||
|
||||
impl Default for Loaders {
|
||||
fn default() -> Self {
|
||||
let include = Arc::new(DefaultBytesLoader::default());
|
||||
Self {
|
||||
bytes: vec![include.clone()],
|
||||
image: Vec::new(),
|
||||
// By default we only include `DefaultTextureLoader`.
|
||||
texture: vec![Arc::new(DefaultTextureLoader::default())],
|
||||
include,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,7 +466,9 @@ impl Focus {
|
||||
}
|
||||
});
|
||||
|
||||
let current_rect = *self.focus_widgets_cache.get(&focus_id).unwrap();
|
||||
let Some(current_rect) = self.focus_widgets_cache.get(&focus_id) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut best_score = std::f32::INFINITY;
|
||||
let mut best_id = None;
|
||||
|
||||
@@ -1590,6 +1590,25 @@ impl Ui {
|
||||
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
|
||||
Image::new(texture_id, size).ui(self)
|
||||
}
|
||||
|
||||
/// Show an image available at the given `uri`.
|
||||
///
|
||||
/// ⚠ This will do nothing unless you install some image loaders first!
|
||||
/// The easiest way to do this is via [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
|
||||
///
|
||||
/// The loaders handle caching image data, sampled textures, etc. across frames, so calling this is immediate-mode safe.
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// ui.image2("file://ferris.svg");
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// See also [`crate::Image2`] and [`crate::ImageSource`].
|
||||
#[inline]
|
||||
pub fn image2<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
|
||||
Image2::new(source.into()).ui(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// # Colors
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::load::Bytes;
|
||||
use crate::{load::SizeHint, load::TexturePoll, *};
|
||||
use emath::Rot2;
|
||||
|
||||
/// An widget to show an image of a given size.
|
||||
@@ -173,3 +176,219 @@ impl Widget for Image {
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget which displays an image.
|
||||
///
|
||||
/// There are three ways to construct this widget:
|
||||
/// - [`Image2::from_uri`]
|
||||
/// - [`Image2::from_bytes`]
|
||||
/// - [`Image2::from_static_bytes`]
|
||||
///
|
||||
/// In both cases the task of actually loading the image
|
||||
/// is deferred to when the `Image2` is added to the [`Ui`].
|
||||
///
|
||||
/// See [`crate::load`] for more information.
|
||||
pub struct Image2<'a> {
|
||||
source: ImageSource<'a>,
|
||||
texture_options: TextureOptions,
|
||||
size_hint: SizeHint,
|
||||
fit: ImageFit,
|
||||
sense: Sense,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
enum ImageFit {
|
||||
// TODO: options for aspect ratio
|
||||
// TODO: other fit strategies
|
||||
// FitToWidth,
|
||||
// FitToHeight,
|
||||
// FitToWidthExact(f32),
|
||||
// FitToHeightExact(f32),
|
||||
#[default]
|
||||
ShrinkToFit,
|
||||
}
|
||||
|
||||
impl ImageFit {
|
||||
pub fn calculate_final_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
|
||||
let aspect_ratio = image_size.x / image_size.y;
|
||||
// TODO: more image sizing options
|
||||
match self {
|
||||
// ImageFit::FitToWidth => todo!(),
|
||||
// ImageFit::FitToHeight => todo!(),
|
||||
// ImageFit::FitToWidthExact(_) => todo!(),
|
||||
// ImageFit::FitToHeightExact(_) => todo!(),
|
||||
ImageFit::ShrinkToFit => {
|
||||
let width = if available_size.x < image_size.x {
|
||||
available_size.x
|
||||
} else {
|
||||
image_size.x
|
||||
};
|
||||
let height = if available_size.y < image_size.y {
|
||||
available_size.y
|
||||
} else {
|
||||
image_size.y
|
||||
};
|
||||
if width < height {
|
||||
Vec2::new(width, width / aspect_ratio)
|
||||
} else {
|
||||
Vec2::new(height * aspect_ratio, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This type tells the [`Ui`] how to load the image.
|
||||
pub enum ImageSource<'a> {
|
||||
/// Load the image from a URI.
|
||||
///
|
||||
/// This could be a `file://` url, `http://` url, or a `bare` identifier.
|
||||
/// How the URI will be turned into a texture for rendering purposes is
|
||||
/// up to the registered loaders to handle.
|
||||
///
|
||||
/// See [`crate::load`] for more information.
|
||||
Uri(&'a str),
|
||||
|
||||
/// Load the image from some raw bytes.
|
||||
///
|
||||
/// The [`Bytes`] may be:
|
||||
/// - `'static`, obtained from `include_bytes!` or similar
|
||||
/// - Anything that can be converted to `Arc<[u8]>`
|
||||
///
|
||||
/// This instructs the [`Ui`] to cache the raw bytes, which are then further processed by any registered loaders.
|
||||
///
|
||||
/// See [`crate::load`] for more information.
|
||||
Bytes(&'static str, Bytes),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for ImageSource<'a> {
|
||||
#[inline]
|
||||
fn from(value: &'a str) -> Self {
|
||||
Self::Uri(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
|
||||
#[inline]
|
||||
fn from((uri, bytes): (&'static str, T)) -> Self {
|
||||
Self::Bytes(uri, bytes.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Image2<'a> {
|
||||
/// Load the image from some source.
|
||||
pub fn new(source: ImageSource<'a>) -> Self {
|
||||
Self {
|
||||
source,
|
||||
texture_options: Default::default(),
|
||||
size_hint: Default::default(),
|
||||
fit: Default::default(),
|
||||
sense: Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the image from a URI.
|
||||
///
|
||||
/// See [`ImageSource::Uri`].
|
||||
pub fn from_uri(uri: &'a str) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Uri(uri),
|
||||
texture_options: Default::default(),
|
||||
size_hint: Default::default(),
|
||||
fit: Default::default(),
|
||||
sense: Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the image from some raw `'static` bytes.
|
||||
///
|
||||
/// For example, you can use this to load an image from bytes obtained via [`include_bytes`].
|
||||
///
|
||||
/// See [`ImageSource::Bytes`].
|
||||
pub fn from_static_bytes(name: &'static str, bytes: &'static [u8]) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Bytes(name, Bytes::Static(bytes)),
|
||||
texture_options: Default::default(),
|
||||
size_hint: Default::default(),
|
||||
fit: Default::default(),
|
||||
sense: Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the image from some raw bytes.
|
||||
///
|
||||
/// See [`ImageSource::Bytes`].
|
||||
pub fn from_bytes(name: &'static str, bytes: impl Into<Arc<[u8]>>) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Bytes(name, Bytes::Shared(bytes.into())),
|
||||
texture_options: Default::default(),
|
||||
size_hint: Default::default(),
|
||||
fit: Default::default(),
|
||||
sense: Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Texture options used when creating the texture.
|
||||
#[inline]
|
||||
pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
|
||||
self.texture_options = texture_options;
|
||||
self
|
||||
}
|
||||
|
||||
/// Size hint used when creating the texture.
|
||||
#[inline]
|
||||
pub fn size_hint(mut self, size_hint: impl Into<SizeHint>) -> Self {
|
||||
self.size_hint = size_hint.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Make the image respond to clicks and/or drags.
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Image2<'a> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let uri = match self.source {
|
||||
ImageSource::Uri(uri) => uri,
|
||||
ImageSource::Bytes(uri, bytes) => {
|
||||
match bytes {
|
||||
Bytes::Static(bytes) => ui.ctx().include_static_bytes(uri, bytes),
|
||||
Bytes::Shared(bytes) => ui.ctx().include_bytes(uri, bytes),
|
||||
}
|
||||
uri
|
||||
}
|
||||
};
|
||||
|
||||
match ui
|
||||
.ctx()
|
||||
.try_load_texture(uri, self.texture_options, self.size_hint)
|
||||
{
|
||||
Ok(TexturePoll::Ready { texture }) => {
|
||||
let final_size = self.fit.calculate_final_size(
|
||||
ui.available_size(),
|
||||
Vec2::new(texture.size[0] as f32, texture.size[1] as f32),
|
||||
);
|
||||
|
||||
let (rect, response) = ui.allocate_exact_size(final_size, self.sense);
|
||||
|
||||
let mut mesh = Mesh::with_texture(texture.id);
|
||||
mesh.add_rect_with_uv(
|
||||
rect,
|
||||
Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
|
||||
Color32::WHITE,
|
||||
);
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
|
||||
response
|
||||
}
|
||||
Ok(TexturePoll::Pending { .. }) => {
|
||||
ui.spinner().on_hover_text(format!("Loading {uri:?}…"))
|
||||
}
|
||||
Err(err) => ui.colored_label(ui.visuals().error_fg_color, err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ pub mod text_edit;
|
||||
pub use button::*;
|
||||
pub use drag_value::DragValue;
|
||||
pub use hyperlink::*;
|
||||
pub use image::Image;
|
||||
pub use image::{Image, Image2, ImageSource};
|
||||
pub use label::*;
|
||||
pub use progress_bar::ProgressBar;
|
||||
pub use selected_label::SelectableLabel;
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.22.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
default-run = "egui_demo_app"
|
||||
|
||||
@@ -45,10 +45,12 @@ log = { version = "0.4", features = ["std"] }
|
||||
# Optional dependencies:
|
||||
|
||||
bytemuck = { version = "1.7.1", optional = true }
|
||||
egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras" }
|
||||
egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras", features = [
|
||||
"log",
|
||||
] }
|
||||
|
||||
# feature "http":
|
||||
ehttp = { version = "0.2.0", optional = true }
|
||||
ehttp = { version = "0.3.0", optional = true }
|
||||
image = { version = "0.24", optional = true, default-features = false, features = [
|
||||
"jpeg",
|
||||
"png",
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.22.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Example library for egui"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
@@ -33,7 +33,9 @@ syntax_highlighting = ["syntect"]
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.22.0", path = "../egui", default-features = false }
|
||||
egui_extras = { version = "0.22.0", path = "../egui_extras" }
|
||||
egui_extras = { version = "0.22.0", path = "../egui_extras", features = [
|
||||
"log",
|
||||
] }
|
||||
egui_plot = { version = "0.22.0", path = "../egui_plot" }
|
||||
enum-map = { version = "2", features = ["serde"] }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
description = "Extra functionality and widgets for the egui GUI library"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
@@ -26,12 +26,18 @@ all-features = true
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## Shorthand for enabling `svg`, `image`, and `ehttp`.
|
||||
all-loaders = ["svg", "image", "http"]
|
||||
|
||||
## Enable [`DatePickerButton`] widget.
|
||||
datepicker = ["chrono"]
|
||||
|
||||
## Log warnings using [`log`](https://docs.rs/log) crate.
|
||||
log = ["dep:log", "egui/log"]
|
||||
|
||||
## Add support for loading images via HTTP.
|
||||
http = ["dep:ehttp"]
|
||||
|
||||
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
|
||||
##
|
||||
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
|
||||
@@ -40,7 +46,6 @@ puffin = ["dep:puffin", "egui/puffin"]
|
||||
## Support loading svg images.
|
||||
svg = ["resvg", "tiny-skia", "usvg"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.22.0", path = "../egui", default-features = false }
|
||||
|
||||
@@ -76,3 +81,6 @@ puffin = { version = "0.16", optional = true }
|
||||
resvg = { version = "0.28", optional = true, default-features = false }
|
||||
tiny-skia = { version = "0.8", optional = true, default-features = false } # must be updated in lock-step with resvg
|
||||
usvg = { version = "0.28", optional = true, default-features = false }
|
||||
|
||||
# http feature
|
||||
ehttp = { version = "0.3.0", optional = true, default-features = false }
|
||||
|
||||
@@ -15,6 +15,7 @@ mod datepicker;
|
||||
|
||||
pub mod image;
|
||||
mod layout;
|
||||
pub mod loaders;
|
||||
mod sizing;
|
||||
mod strip;
|
||||
mod table;
|
||||
@@ -85,3 +86,31 @@ macro_rules! log_or_panic {
|
||||
}};
|
||||
}
|
||||
pub(crate) use log_or_panic;
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! log_warn {
|
||||
($fmt: literal) => {$crate::log_warn!($fmt,)};
|
||||
($fmt: literal, $($arg: tt)*) => {{
|
||||
#[cfg(feature = "log")]
|
||||
log::warn!($fmt, $($arg)*);
|
||||
|
||||
#[cfg(not(feature = "log"))]
|
||||
println!(
|
||||
concat!("egui_extras: warning: ", $fmt), $($arg)*
|
||||
)
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use log_warn;
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! log_trace {
|
||||
($fmt: literal) => {$crate::log_trace!($fmt,)};
|
||||
($fmt: literal, $($arg: tt)*) => {{
|
||||
#[cfg(feature = "log")]
|
||||
log::trace!($fmt, $($arg)*);
|
||||
}};
|
||||
}
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use log_trace;
|
||||
|
||||
58
crates/egui_extras/src/loaders.rs
Normal file
58
crates/egui_extras/src/loaders.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
// TODO: automatic cache eviction
|
||||
|
||||
/// Installs the default set of loaders:
|
||||
/// - `file` loader on non-Wasm targets
|
||||
/// - `http` loader (with the `ehttp` feature)
|
||||
/// - `image` loader (with the `image` feature)
|
||||
/// - `svg` loader with the `svg` feature
|
||||
///
|
||||
/// ⚠ This will do nothing and you won't see any images unless you enable some features!
|
||||
/// If you just want to be able to load `file://` and `http://` images, enable the `all-loaders` feature.
|
||||
///
|
||||
/// ⚠ The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image)
|
||||
/// crate as your direct dependency, and enabling features on it:
|
||||
///
|
||||
/// ```toml,ignore
|
||||
/// image = { version = "0.24", features = ["jpeg", "png"] }
|
||||
/// ```
|
||||
///
|
||||
/// See [`egui::load`] for more information about how loaders work.
|
||||
pub fn install(ctx: &egui::Context) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default()));
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
ctx.add_bytes_loader(std::sync::Arc::new(
|
||||
self::ehttp_loader::EhttpLoader::default(),
|
||||
));
|
||||
|
||||
#[cfg(feature = "image")]
|
||||
ctx.add_image_loader(std::sync::Arc::new(
|
||||
self::image_loader::ImageCrateLoader::default(),
|
||||
));
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
|
||||
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
not(feature = "http"),
|
||||
not(feature = "image"),
|
||||
not(feature = "svg")
|
||||
))]
|
||||
crate::log_warn!("`loaders::install` was called, but no loaders are enabled");
|
||||
|
||||
let _ = ctx;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod file_loader;
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
mod ehttp_loader;
|
||||
|
||||
#[cfg(feature = "image")]
|
||||
mod image_loader;
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
mod svg_loader;
|
||||
107
crates/egui_extras/src/loaders/ehttp_loader.rs
Normal file
107
crates/egui_extras/src/loaders/ehttp_loader.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use egui::{
|
||||
ahash::HashMap,
|
||||
load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError},
|
||||
mutex::Mutex,
|
||||
};
|
||||
use std::{sync::Arc, task::Poll};
|
||||
|
||||
type Entry = Poll<Result<Arc<[u8]>, String>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EhttpLoader {
|
||||
cache: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
}
|
||||
|
||||
const PROTOCOLS: &[&str] = &["http://", "https://"];
|
||||
|
||||
fn starts_with_one_of(s: &str, prefixes: &[&str]) -> bool {
|
||||
prefixes.iter().any(|prefix| s.starts_with(prefix))
|
||||
}
|
||||
|
||||
fn get_image_bytes(
|
||||
uri: &str,
|
||||
response: Result<ehttp::Response, String>,
|
||||
) -> Result<Arc<[u8]>, String> {
|
||||
let response = response?;
|
||||
if !response.ok {
|
||||
match response.text() {
|
||||
Some(response_text) => {
|
||||
return Err(format!(
|
||||
"failed to load {uri:?}: {} {} {response_text}",
|
||||
response.status, response.status_text
|
||||
))
|
||||
}
|
||||
None => {
|
||||
return Err(format!(
|
||||
"failed to load {uri:?}: {} {}",
|
||||
response.status, response.status_text
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(content_type) = response.content_type() else {
|
||||
return Err(format!("failed to load {uri:?}: no content-type header found"));
|
||||
};
|
||||
if !content_type.starts_with("image/") {
|
||||
return Err(format!("failed to load {uri:?}: expected content-type starting with \"image/\", found {content_type:?}"));
|
||||
}
|
||||
|
||||
Ok(response.bytes.into())
|
||||
}
|
||||
|
||||
impl BytesLoader for EhttpLoader {
|
||||
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
|
||||
if !starts_with_one_of(uri, PROTOCOLS) {
|
||||
return Err(LoadError::NotSupported);
|
||||
}
|
||||
|
||||
let mut cache = self.cache.lock();
|
||||
if let Some(entry) = cache.get(uri).cloned() {
|
||||
match entry {
|
||||
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
|
||||
size: None,
|
||||
bytes: Bytes::Shared(bytes),
|
||||
}),
|
||||
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
|
||||
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
|
||||
}
|
||||
} else {
|
||||
crate::log_trace!("started loading {uri:?}");
|
||||
|
||||
let uri = uri.to_owned();
|
||||
cache.insert(uri.clone(), Poll::Pending);
|
||||
drop(cache);
|
||||
|
||||
ehttp::fetch(ehttp::Request::get(uri.clone()), {
|
||||
let ctx = ctx.clone();
|
||||
let cache = self.cache.clone();
|
||||
move |response| {
|
||||
let result = get_image_bytes(&uri, response);
|
||||
crate::log_trace!("finished loading {uri:?}");
|
||||
let prev = cache.lock().insert(uri, Poll::Ready(result));
|
||||
assert!(matches!(prev, Some(Poll::Pending)));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(BytesPoll::Pending { size: None })
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|entry| match entry {
|
||||
Poll::Ready(Ok(bytes)) => bytes.len(),
|
||||
Poll::Ready(Err(err)) => err.len(),
|
||||
_ => 0,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
84
crates/egui_extras/src/loaders/file_loader.rs
Normal file
84
crates/egui_extras/src/loaders/file_loader.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use egui::{
|
||||
ahash::HashMap,
|
||||
load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError},
|
||||
mutex::Mutex,
|
||||
};
|
||||
use std::{sync::Arc, task::Poll, thread};
|
||||
|
||||
type Entry = Poll<Result<Arc<[u8]>, String>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FileLoader {
|
||||
/// Cache for loaded files
|
||||
cache: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
}
|
||||
|
||||
const PROTOCOL: &str = "file://";
|
||||
|
||||
impl BytesLoader for FileLoader {
|
||||
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
|
||||
// File loader only supports the `file` protocol.
|
||||
let Some(path) = uri.strip_prefix(PROTOCOL) else {
|
||||
return Err(LoadError::NotSupported);
|
||||
};
|
||||
|
||||
let mut cache = self.cache.lock();
|
||||
if let Some(entry) = cache.get(path).cloned() {
|
||||
// `path` has either begun loading, is loaded, or has failed to load.
|
||||
match entry {
|
||||
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
|
||||
size: None,
|
||||
bytes: Bytes::Shared(bytes),
|
||||
}),
|
||||
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
|
||||
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
|
||||
}
|
||||
} else {
|
||||
crate::log_trace!("started loading {uri:?}");
|
||||
// We need to load the file at `path`.
|
||||
|
||||
// Set the file to `pending` until we finish loading it.
|
||||
let path = path.to_owned();
|
||||
cache.insert(path.clone(), Poll::Pending);
|
||||
drop(cache);
|
||||
|
||||
// Spawn a thread to read the file, so that we don't block the render for too long.
|
||||
thread::Builder::new()
|
||||
.name(format!("egui_extras::FileLoader::load({uri:?})"))
|
||||
.spawn({
|
||||
let ctx = ctx.clone();
|
||||
let cache = self.cache.clone();
|
||||
let uri = uri.to_owned();
|
||||
move || {
|
||||
let result = match std::fs::read(&path) {
|
||||
Ok(bytes) => Ok(bytes.into()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
let prev = cache.lock().insert(path, Poll::Ready(result));
|
||||
assert!(matches!(prev, Some(Poll::Pending)));
|
||||
ctx.request_repaint();
|
||||
crate::log_trace!("finished loading {uri:?}");
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn thread");
|
||||
|
||||
Ok(BytesPoll::Pending { size: None })
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|entry| match entry {
|
||||
Poll::Ready(Ok(bytes)) => bytes.len(),
|
||||
Poll::Ready(Err(err)) => err.len(),
|
||||
_ => 0,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
84
crates/egui_extras/src/loaders/image_loader.rs
Normal file
84
crates/egui_extras/src/loaders/image_loader.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use egui::{
|
||||
ahash::HashMap,
|
||||
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
|
||||
mutex::Mutex,
|
||||
ColorImage,
|
||||
};
|
||||
use std::{mem::size_of, path::Path, sync::Arc};
|
||||
|
||||
type Entry = Result<Arc<ColorImage>, String>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ImageCrateLoader {
|
||||
cache: Mutex<HashMap<String, Entry>>,
|
||||
}
|
||||
|
||||
fn is_supported(uri: &str) -> bool {
|
||||
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else {
|
||||
// `true` because if there's no extension, assume that we support it
|
||||
return true
|
||||
};
|
||||
|
||||
ext != "svg"
|
||||
}
|
||||
|
||||
impl ImageLoader for ImageCrateLoader {
|
||||
fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult {
|
||||
if !is_supported(uri) {
|
||||
return Err(LoadError::NotSupported);
|
||||
}
|
||||
|
||||
let mut cache = self.cache.lock();
|
||||
if let Some(entry) = cache.get(uri).cloned() {
|
||||
match entry {
|
||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
||||
Err(err) => Err(LoadError::Custom(err)),
|
||||
}
|
||||
} else {
|
||||
match ctx.try_load_bytes(uri) {
|
||||
Ok(BytesPoll::Ready { bytes, .. }) => {
|
||||
crate::log_trace!("started loading {uri:?}");
|
||||
let result = crate::image::load_image_bytes(&bytes).map(Arc::new);
|
||||
crate::log_trace!("finished loading {uri:?}");
|
||||
cache.insert(uri.into(), result.clone());
|
||||
match result {
|
||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
||||
Err(err) => Err(LoadError::Custom(err)),
|
||||
}
|
||||
}
|
||||
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|result| match result {
|
||||
Ok(image) => image.pixels.len() * size_of::<egui::Color32>(),
|
||||
Err(err) => err.len(),
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn check_support() {
|
||||
assert!(is_supported("https://test.png"));
|
||||
assert!(is_supported("test.jpeg"));
|
||||
assert!(is_supported("http://test.gif"));
|
||||
assert!(is_supported("test.webp"));
|
||||
assert!(is_supported("file://test"));
|
||||
assert!(!is_supported("test.svg"));
|
||||
}
|
||||
}
|
||||
92
crates/egui_extras/src/loaders/svg_loader.rs
Normal file
92
crates/egui_extras/src/loaders/svg_loader.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use egui::{
|
||||
ahash::HashMap,
|
||||
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
|
||||
mutex::Mutex,
|
||||
ColorImage,
|
||||
};
|
||||
use std::{mem::size_of, path::Path, sync::Arc};
|
||||
|
||||
type Entry = Result<Arc<ColorImage>, String>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SvgLoader {
|
||||
cache: Mutex<HashMap<(String, SizeHint), Entry>>,
|
||||
}
|
||||
|
||||
fn is_supported(uri: &str) -> bool {
|
||||
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else { return false };
|
||||
|
||||
ext == "svg"
|
||||
}
|
||||
|
||||
impl ImageLoader for SvgLoader {
|
||||
fn load(&self, ctx: &egui::Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult {
|
||||
if !is_supported(uri) {
|
||||
return Err(LoadError::NotSupported);
|
||||
}
|
||||
|
||||
let uri = uri.to_owned();
|
||||
|
||||
let mut cache = self.cache.lock();
|
||||
// We can't avoid the `uri` clone here without unsafe code.
|
||||
if let Some(entry) = cache.get(&(uri.clone(), size_hint)).cloned() {
|
||||
match entry {
|
||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
||||
Err(err) => Err(LoadError::Custom(err)),
|
||||
}
|
||||
} else {
|
||||
match ctx.try_load_bytes(&uri) {
|
||||
Ok(BytesPoll::Ready { bytes, .. }) => {
|
||||
crate::log_trace!("started loading {uri:?}");
|
||||
let fit_to = match size_hint {
|
||||
SizeHint::Original => usvg::FitTo::Original,
|
||||
SizeHint::Width(w) => usvg::FitTo::Width(w),
|
||||
SizeHint::Height(h) => usvg::FitTo::Height(h),
|
||||
SizeHint::Size(w, h) => usvg::FitTo::Size(w, h),
|
||||
};
|
||||
let result =
|
||||
crate::image::load_svg_bytes_with_size(&bytes, fit_to).map(Arc::new);
|
||||
crate::log_trace!("finished loading {uri:?}");
|
||||
cache.insert((uri, size_hint), result.clone());
|
||||
match result {
|
||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
||||
Err(err) => Err(LoadError::Custom(err)),
|
||||
}
|
||||
}
|
||||
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
self.cache.lock().retain(|(u, _), _| u != uri);
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|result| match result {
|
||||
Ok(image) => image.pixels.len() * size_of::<egui::Color32>(),
|
||||
Err(err) => err.len(),
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn check_support() {
|
||||
// inverse of same test in `image_loader.rs`
|
||||
assert!(!is_supported("https://test.png"));
|
||||
assert!(!is_supported("test.jpeg"));
|
||||
assert!(!is_supported("http://test.gif"));
|
||||
assert!(!is_supported("test.webp"));
|
||||
assert!(!is_supported("file://test"));
|
||||
assert!(is_supported("test.svg"));
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ version = "0.22.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Bindings for using egui natively using the glium library"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_glium"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.22.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Bindings for using egui natively using the glow library"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_glow"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
description = "Immediate mode plotting for the egui GUI library"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -255,9 +255,8 @@ impl AxisWidget {
|
||||
|
||||
// --- add ticks ---
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let transform = match self.transform {
|
||||
Some(t) => t,
|
||||
None => return response,
|
||||
let Some(transform) = self.transform else {
|
||||
return response;
|
||||
};
|
||||
|
||||
for step in self.steps.iter() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Contains items that can be added to a plot.
|
||||
#![allow(clippy::type_complexity)] // TODO(emilk): simplify some of the callback types with type aliases
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.22.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Minimal 2D math library for GUI work"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/emath"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -360,9 +360,17 @@ impl Rect {
|
||||
/// Signed distance to the edge of the box.
|
||||
///
|
||||
/// Negative inside the box.
|
||||
///
|
||||
/// ```
|
||||
/// # use emath::{pos2, Rect};
|
||||
/// let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
||||
/// assert_eq!(rect.signed_distance_to_pos(pos2(0.50, 0.50)), -0.50);
|
||||
/// assert_eq!(rect.signed_distance_to_pos(pos2(0.75, 0.50)), -0.25);
|
||||
/// assert_eq!(rect.signed_distance_to_pos(pos2(1.50, 0.50)), 0.50);
|
||||
/// ```
|
||||
pub fn signed_distance_to_pos(&self, pos: Pos2) -> f32 {
|
||||
let edge_distances = (pos - self.center()).abs() - self.size() * 0.5;
|
||||
let inside_dist = edge_distances.x.max(edge_distances.y).min(0.0);
|
||||
let inside_dist = edge_distances.max_elem().min(0.0);
|
||||
let outside_dist = edge_distances.max(Vec2::ZERO).length();
|
||||
inside_dist + outside_dist
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.22.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
description = "Minimal 2D graphics library for GUI work"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
homepage = "https://github.com/emilk/egui/tree/master/crates/epaint"
|
||||
license = "(MIT OR Apache-2.0) AND OFL-1.1 AND LicenseRef-UFL-1.0" # OFL and UFL used by default_fonts. See https://github.com/emilk/egui/issues/2321
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{textures::TextureOptions, Color32};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// An image stored in RAM.
|
||||
///
|
||||
@@ -11,7 +12,7 @@ use crate::{textures::TextureOptions, Color32};
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum ImageData {
|
||||
/// RGBA image.
|
||||
Color(ColorImage),
|
||||
Color(Arc<ColorImage>),
|
||||
|
||||
/// Used for the font texture.
|
||||
Font(FontImage),
|
||||
@@ -226,6 +227,13 @@ impl std::ops::IndexMut<(usize, usize)> for ColorImage {
|
||||
impl From<ColorImage> for ImageData {
|
||||
#[inline(always)]
|
||||
fn from(image: ColorImage) -> Self {
|
||||
Self::Color(Arc::new(image))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<ColorImage>> for ImageData {
|
||||
#[inline]
|
||||
fn from(image: Arc<ColorImage>) -> Self {
|
||||
Self::Color(image)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![allow(clippy::derive_hash_xor_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
|
||||
#![allow(clippy::derived_hash_with_manual_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ impl FontId {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)]
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
impl std::hash::Hash for FontId {
|
||||
#[inline(always)]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
@@ -552,7 +552,7 @@ impl FontsAndCache {
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct HashableF32(f32);
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)]
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
impl std::hash::Hash for HashableF32 {
|
||||
#[inline(always)]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
|
||||
@@ -320,9 +320,8 @@ fn replace_last_glyph_with_overflow_character(
|
||||
job: &LayoutJob,
|
||||
row: &mut Row,
|
||||
) {
|
||||
let overflow_character = match job.wrap.overflow_character {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
let Some(overflow_character) = job.wrap.overflow_character else {
|
||||
return;
|
||||
};
|
||||
|
||||
loop {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![allow(clippy::derive_hash_xor_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
|
||||
#![allow(clippy::derived_hash_with_manual_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine
|
||||
#![allow(clippy::wrong_self_convention)] // We use `from_` to indicate conversion direction. It's non-diomatic, but makes sense in this context.
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
@@ -95,6 +95,11 @@ impl TextureHandle {
|
||||
crate::Vec2::new(w as f32, h as f32)
|
||||
}
|
||||
|
||||
/// `width x height x bytes_per_pixel`
|
||||
pub fn byte_size(&self) -> usize {
|
||||
self.tex_mngr.read().meta(self.id).unwrap().bytes_used()
|
||||
}
|
||||
|
||||
/// width / height
|
||||
pub fn aspect_ratio(&self) -> f32 {
|
||||
let [w, h] = self.size();
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
authors = ["tami5 <kkharji@proton.me>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@ publish = false
|
||||
eframe = { path = "../../crates/eframe", features = [
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
] }
|
||||
egui_extras = { path = "../../crates/egui_extras", features = ["image"] }
|
||||
ehttp = "0.2"
|
||||
egui_extras = { path = "../../crates/egui_extras", features = [
|
||||
"http",
|
||||
"image",
|
||||
"log",
|
||||
] }
|
||||
env_logger = "0.10"
|
||||
image = { version = "0.24", default-features = false, features = ["jpeg"] }
|
||||
poll-promise = "0.2"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use eframe::egui;
|
||||
use egui_extras::RetainedImage;
|
||||
use poll_promise::Promise;
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
@@ -10,55 +8,34 @@ fn main() -> Result<(), eframe::Error> {
|
||||
eframe::run_native(
|
||||
"Download and show an image with eframe/egui",
|
||||
options,
|
||||
Box::new(|_cc| Box::<MyApp>::default()),
|
||||
Box::new(|cc| {
|
||||
// Without the following call, the `Image2` created below
|
||||
// will simply output `not supported` error messages.
|
||||
egui_extras::loaders::install(&cc.egui_ctx);
|
||||
Box::new(MyApp)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyApp {
|
||||
/// `None` when download hasn't started yet.
|
||||
promise: Option<Promise<ehttp::Result<RetainedImage>>>,
|
||||
}
|
||||
struct MyApp;
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
let promise = self.promise.get_or_insert_with(|| {
|
||||
// Begin download.
|
||||
// We download the image using `ehttp`, a library that works both in WASM and on native.
|
||||
// We use the `poll-promise` library to communicate with the UI thread.
|
||||
let ctx = ctx.clone();
|
||||
let (sender, promise) = Promise::new();
|
||||
let request = ehttp::Request::get("https://picsum.photos/seed/1.759706314/1024");
|
||||
ehttp::fetch(request, move |response| {
|
||||
let image = response.and_then(parse_response);
|
||||
sender.send(image); // send the results back to the UI thread.
|
||||
ctx.request_repaint(); // wake up UI thread
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let width = ui.available_width();
|
||||
let half_height = ui.available_height() / 2.0;
|
||||
|
||||
ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| {
|
||||
ui.add(egui::Image2::from_uri(
|
||||
"https://picsum.photos/seed/1.759706314/1024",
|
||||
))
|
||||
});
|
||||
ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| {
|
||||
ui.add(egui::Image2::from_uri(
|
||||
"https://this-is-hopefully-not-a-real-website.rs/image.png",
|
||||
))
|
||||
});
|
||||
promise
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| match promise.ready() {
|
||||
None => {
|
||||
ui.spinner(); // still loading
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
ui.colored_label(ui.visuals().error_fg_color, err); // something went wrong
|
||||
}
|
||||
Some(Ok(image)) => {
|
||||
image.show_max_size(ui, ui.available_size());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn parse_response(response: ehttp::Response) -> Result<RetainedImage, String> {
|
||||
let content_type = response.content_type().unwrap_or_default();
|
||||
if content_type.starts_with("image/") {
|
||||
RetainedImage::from_image_bytes(&response.url, &response.bytes)
|
||||
} else {
|
||||
Err(format!(
|
||||
"Expected image, found content-type {content_type:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@ publish = false
|
||||
eframe = { path = "../../crates/eframe", features = [
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
] }
|
||||
egui_extras = { path = "../../crates/egui_extras", features = ["image"] }
|
||||
egui_extras = { path = "../../crates/egui_extras", features = ["image", "log"] }
|
||||
env_logger = "0.10"
|
||||
image = { version = "0.24", default-features = false, features = ["png"] }
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
authors = ["hacknus <l_stoeckli@bluewin.ch>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -7,7 +7,7 @@ authors = [
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -17,5 +17,4 @@ eframe = { path = "../../crates/eframe", features = [
|
||||
"wgpu",
|
||||
] }
|
||||
env_logger = "0.10"
|
||||
itertools = "0.10.3"
|
||||
image = { version = "0.24", default-features = false, features = ["png"] }
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
|
||||
@@ -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.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ publish = false
|
||||
eframe = { path = "../../crates/eframe", features = [
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
] }
|
||||
egui_extras = { path = "../../crates/egui_extras", features = ["svg"] }
|
||||
egui_extras = { path = "../../crates/egui_extras", features = ["log", "svg"] }
|
||||
env_logger = "0.10"
|
||||
|
||||
@@ -15,26 +15,16 @@ fn main() -> Result<(), eframe::Error> {
|
||||
eframe::run_native(
|
||||
"svg example",
|
||||
options,
|
||||
Box::new(|_cc| Box::<MyApp>::default()),
|
||||
Box::new(|cc| {
|
||||
// Without the following call, the `Image2` created below
|
||||
// will simply output a `not supported` error message.
|
||||
egui_extras::loaders::install(&cc.egui_ctx);
|
||||
Box::new(MyApp)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
struct MyApp {
|
||||
svg_image: egui_extras::RetainedImage,
|
||||
}
|
||||
|
||||
impl Default for MyApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
svg_image: egui_extras::RetainedImage::from_svg_bytes_with_size(
|
||||
"rustacean-flat-happy.svg",
|
||||
include_bytes!("rustacean-flat-happy.svg"),
|
||||
egui_extras::image::FitTo::Original,
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
struct MyApp;
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
@@ -45,7 +35,13 @@ impl eframe::App for MyApp {
|
||||
ui.separator();
|
||||
|
||||
let max_size = ui.available_size();
|
||||
self.svg_image.show_size(ui, max_size);
|
||||
ui.add(
|
||||
egui::Image2::from_static_bytes(
|
||||
"ferris.svg",
|
||||
include_bytes!("rustacean-flat-happy.svg"),
|
||||
)
|
||||
.size_hint(max_size),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
authors = ["TicClick <ya@ticclick.ch>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
# to the user in the error, instead of "error: invalid channel name '[toolchain]'".
|
||||
|
||||
[toolchain]
|
||||
channel = "1.67.0"
|
||||
channel = "1.70.0"
|
||||
components = [ "rustfmt", "clippy" ]
|
||||
targets = [ "wasm32-unknown-unknown" ]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Section identical to the root clippy.toml:
|
||||
|
||||
msrv = "1.67"
|
||||
msrv = "1.70"
|
||||
|
||||
allow-unwrap-in-tests = true
|
||||
|
||||
@@ -17,9 +17,10 @@ avoid-breaking-exported-api = false
|
||||
max-fn-params-bools = 2 # TODO(emilk): decrease this to 1
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/large_include_file
|
||||
max-include-file-size = 100000
|
||||
max-include-file-size = 1000000
|
||||
|
||||
too-many-lines-threshold = 100
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/type_complexity
|
||||
type-complexity-threshold = 350
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user