1
0
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:
Konkitoman
2023-09-19 17:22:31 +03:00
91 changed files with 3056 additions and 1525 deletions

View File

@@ -49,7 +49,7 @@ jobs:
run: cargo check --locked --all-features --all-targets
- name: check egui_extras --all-features
run: cargo check --locked --all-features --all-targets -p egui_extras
run: cargo check --locked --all-features -p egui_extras
- name: check default features
run: cargo check --locked --all-targets
@@ -57,11 +57,14 @@ jobs:
- name: check --no-default-features
run: cargo check --locked --no-default-features --lib --all-targets
- name: check epaint --no-default-features
run: cargo check --locked --no-default-features --lib --all-targets -p epaint
- name: check eframe --no-default-features
run: cargo check --locked --no-default-features --features x11 --lib --all-targets -p eframe
run: cargo check --locked --no-default-features --features x11 --lib -p eframe
- name: check egui_extras --no-default-features
run: cargo check --locked --no-default-features --lib -p egui_extras
- name: check epaint --no-default-features
run: cargo check --locked --no-default-features --lib -p epaint
- name: Test doc-tests
run: cargo test --doc --all-features

70
Cargo.lock generated
View File

@@ -1083,16 +1083,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
[[package]]
name = "download_image"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
"image",
]
[[package]]
name = "dyn-clonable"
version = "0.9.0"
@@ -1232,6 +1222,7 @@ dependencies = [
"image",
"log",
"poll-promise",
"rfd",
"serde",
"wasm-bindgen",
"wasm-bindgen-futures",
@@ -1248,10 +1239,8 @@ dependencies = [
"egui",
"egui_extras",
"egui_plot",
"enum-map",
"log",
"serde",
"syntect",
"unicode_names2",
]
@@ -1263,11 +1252,14 @@ dependencies = [
"document-features",
"egui",
"ehttp",
"enum-map",
"image",
"log",
"mime_guess",
"puffin",
"resvg",
"serde",
"syntect",
"tiny-skia",
"usvg",
]
@@ -2027,6 +2019,16 @@ dependencies = [
"png",
]
[[package]]
name = "images"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
"image",
]
[[package]]
name = "imagesize"
version = "0.10.1"
@@ -2311,6 +2313,22 @@ dependencies = [
"paste",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -2999,16 +3017,6 @@ dependencies = [
"usvg",
]
[[package]]
name = "retained_image"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
"image",
]
[[package]]
name = "rfd"
version = "0.11.4"
@@ -3409,15 +3417,6 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "svg"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
]
[[package]]
name = "svgtypes"
version = "0.8.2"
@@ -3748,6 +3747,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.13"

View File

@@ -198,55 +198,7 @@ These are the official egui integrations:
Missing an integration for the thing you're working on? Create one, it's easy!
### Writing your own egui integration
You need to collect [`egui::RawInput`](https://docs.rs/egui/latest/egui/struct.RawInput.html) and handle [`egui::FullOutput`](https://docs.rs/egui/latest/egui/struct.FullOutput.html). The basic structure is this:
``` rust
let mut egui_ctx = egui::CtxRef::default();
// Game loop:
loop {
// Gather input (mouse, touches, keyboard, screen size, etc):
let raw_input: egui::RawInput = my_integration.gather_input();
let full_output = egui_ctx.run(raw_input, |egui_ctx| {
my_app.ui(egui_ctx); // add panels, windows and widgets to `egui_ctx` here
});
let clipped_primitives = egui_ctx.tessellate(full_output.shapes); // creates triangles to paint
my_integration.paint(&full_output.textures_delta, clipped_primitives);
let platform_output = full_output.platform_output;
my_integration.set_cursor_icon(platform_output.cursor_icon);
if !platform_output.copied_text.is_empty() {
my_integration.set_clipboard_text(platform_output.copied_text);
}
// See `egui::FullOutput` and `egui::PlatformOutput` for more
}
```
For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/crates/egui_glium/src/painter.rs) or [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs).
### Debugging your integration
#### Things look jagged
* Turn off backface culling.
#### My text is blurry
* Make sure you set the proper `pixels_per_point` in the input to egui.
* Make sure the texture sampler is not off by half a pixel. Try nearest-neighbor sampler to check.
#### My windows are too transparent or too dark
* egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`.
* Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`).
* egui prefers linear color spaces for all blending so:
* Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`).
* Otherwise: remember to decode gamma in the fragment shader.
* Decode the gamma of the incoming vertex colors in your vertex shader.
* Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`).
* Otherwise: gamma-encode the colors before you write them again.
See <https://docs.rs/egui/latest/egui/#integrating-with-egui>.
## Why immediate mode

View File

@@ -60,4 +60,4 @@ Not all rust crates work when compiled to WASM, but here are some useful crates
## Name
The _frame_ in `eframe` stands both for the frame in which your `egui` app resides and also for "framework" (`frame` is a framework, `egui` is a library).
The _frame_ in `eframe` stands both for the frame in which your `egui` app resides and also for "framework" (`eframe` is a framework, `egui` is a library).

View File

@@ -1158,6 +1158,7 @@ impl Storage for DummyStorage {
/// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key.
#[cfg(feature = "ron")]
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
crate::profile_function!(key);
storage
.get_string(key)
.and_then(|value| match ron::from_str(&value) {
@@ -1172,6 +1173,7 @@ pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &st
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
#[cfg(feature = "ron")]
pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, value: &T) {
crate::profile_function!(key);
match ron::ser::to_string(value) {
Ok(string) => storage.set_string(key, string),
Err(err) => log::error!("eframe failed to encode data using ron: {}", err),

View File

@@ -324,7 +324,6 @@ pub type Result<T> = std::result::Result<T, Error>;
// ---------------------------------------------------------------------------
#[cfg(not(target_arch = "wasm32"))]
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
@@ -333,6 +332,7 @@ mod profiling_scopes {
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
@@ -342,11 +342,12 @@ mod profiling_scopes {
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(unused_imports)]
pub(crate) use profiling_scopes::*;

View File

@@ -191,6 +191,8 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
crate::profile_function!();
use cocoa::{
appkit::{NSApp, NSApplication, NSImage, NSMenu, NSWindow},
base::{id, nil},
@@ -221,12 +223,15 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
png_bytes.len() as u64,
);
let app_icon = NSImage::initWithData_(NSImage::alloc(nil), data);
crate::profile_scope!("setApplicationIconImage_");
app.setApplicationIconImage_(app_icon);
}
// Change the title in the top bar - for python processes this would be again "python" otherwise.
let main_menu = app.mainMenu();
let app_menu: id = msg_send![main_menu.itemAtIndex_(0), submenu];
crate::profile_scope!("setTitle_");
app_menu.setTitle_(NSString::alloc(nil).init_str(title));
// The title in the Dock apparently can't be changed.

View File

@@ -178,6 +178,7 @@ pub fn apply_native_options_to_window(
native_options: &crate::NativeOptions,
window_settings: Option<WindowSettings>,
) {
crate::profile_function!();
use winit::window::WindowLevel;
window.set_window_level(if native_options.always_on_top {
WindowLevel::AlwaysOnTop
@@ -443,6 +444,8 @@ impl EpiIntegration {
egui_winit: &mut egui_winit::State,
viewport_id: ViewportId,
) -> EventResponse {
crate::profile_function!();
use winit::event::{ElementState, MouseButton, WindowEvent};
match event {
@@ -614,6 +617,7 @@ const STORAGE_EGUI_MEMORY_KEY: &str = "egui";
const STORAGE_WINDOW_KEY: &str = "window";
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
crate::profile_function!();
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_WINDOW_KEY)
@@ -623,6 +627,7 @@ pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<Windo
}
pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option<egui::Memory> {
crate::profile_function!();
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)

View File

@@ -1,5 +1,6 @@
use std::{
collections::HashMap,
io::Write,
path::{Path, PathBuf},
};
@@ -31,6 +32,7 @@ pub struct FileStorage {
impl Drop for FileStorage {
fn drop(&mut self) {
if let Some(join_handle) = self.last_save_join_handle.take() {
crate::profile_scope!("wait_for_save");
join_handle.join().ok();
}
}
@@ -39,6 +41,7 @@ impl Drop for FileStorage {
impl FileStorage {
/// Store the state in this .ron file.
fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
crate::profile_function!();
let ron_filepath: PathBuf = ron_filepath.into();
log::debug!("Loading app state from {:?}…", ron_filepath);
Self {
@@ -51,6 +54,7 @@ impl FileStorage {
/// Find a good place to put the files that the OS likes.
pub fn from_app_id(app_id: &str) -> Option<Self> {
crate::profile_function!(app_id);
if let Some(data_dir) = storage_dir(app_id) {
if let Err(err) = std::fs::create_dir_all(&data_dir) {
log::warn!(
@@ -83,6 +87,7 @@ impl crate::Storage for FileStorage {
fn flush(&mut self) {
if self.dirty {
crate::profile_function!();
self.dirty = false;
let file_path = self.ron_filepath.clone();
@@ -122,10 +127,13 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
match std::fs::File::create(file_path) {
Ok(file) => {
let mut writer = std::io::BufWriter::new(file);
let config = Default::default();
if let Err(err) = ron::ser::to_writer_pretty(file, &kv, config) {
log::warn!("Failed to serialize app state: {err}");
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
.and_then(|_| writer.flush().map_err(|err| err.into()))
{
log::warn!("Failed to serialize app state: {}", err);
} else {
log::trace!("Persisted to {:?}", file_path);
}
@@ -142,6 +150,7 @@ fn read_ron<T>(ron_path: impl AsRef<Path>) -> Option<T>
where
T: serde::de::DeserializeOwned,
{
crate::profile_function!();
match std::fs::File::open(ron_path) {
Ok(file) => {
let reader = std::io::BufReader::new(file);

View File

@@ -120,6 +120,7 @@ trait WinitApp {
fn create_event_loop_builder(
native_options: &mut epi::NativeOptions,
) -> EventLoopBuilder<UserEvent> {
crate::profile_function!();
let mut event_loop_builder = winit::event_loop::EventLoopBuilder::with_user_event();
if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) {
@@ -129,6 +130,14 @@ fn create_event_loop_builder(
event_loop_builder
}
fn create_event_loop(native_options: &mut epi::NativeOptions) -> EventLoop<UserEvent> {
crate::profile_function!();
let mut builder = create_event_loop_builder(native_options);
crate::profile_scope!("EventLoopBuilder::build");
builder.build()
}
/// Access a thread-local event loop.
///
/// We reuse the event-loop so we can support closing and opening an eframe window
@@ -145,8 +154,7 @@ fn with_event_loop<R>(
// do that as part of the lazy thread local storage initialization and so we instead
// create the event loop lazily here
let mut event_loop = event_loop.borrow_mut();
let event_loop = event_loop
.get_or_insert_with(|| create_event_loop_builder(&mut native_options).build());
let event_loop = event_loop.get_or_insert_with(|| create_event_loop(&mut native_options));
f(event_loop, native_options)
})
}
@@ -165,6 +173,8 @@ fn run_and_return(
let mut returned_result = Ok(());
event_loop.run_return(|event, event_loop, control_flow| {
crate::profile_scope!("winit_event", short_event_description(&event));
WINIT_EVENT_LOOP.with(|row_event_loop| *row_event_loop.write() = event_loop);
let events = match &event {
@@ -324,6 +334,8 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
let mut windows_next_repaint_times = HashMap::default();
event_loop.run(move |event, event_loop, control_flow| {
crate::profile_scope!("winit_event", short_event_description(&event));
WINIT_EVENT_LOOP.with(|row_event_loop| *row_event_loop.write() = event_loop);
let events = match event {
@@ -536,6 +548,8 @@ mod glow_integration {
native_options: &epi::NativeOptions,
event_loop: &EventLoopWindowTarget<UserEvent>,
) -> Result<Self> {
crate::profile_function!();
use glutin::prelude::*;
// convert native options to glutin options
let hardware_acceleration = match native_options.hardware_acceleration {
@@ -576,26 +590,35 @@ mod glow_integration {
"trying to create glutin Display with config: {:?}",
&config_template_builder
);
// create gl display. this may probably create a window too on most platforms. definitely on `MS windows`. never on android.
let (window, gl_config) = glutin_winit::DisplayBuilder::new()
// Create GL display. This may probably create a window too on most platforms. Definitely on `MS windows`. Never on Android.
let display_builder = glutin_winit::DisplayBuilder::new()
// we might want to expose this option to users in the future. maybe using an env var or using native_options.
.with_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150
.with_window_builder(Some(create_winit_window_builder(&window_builder)))
.build(
event_loop,
config_template_builder.clone(),
|mut config_iterator| {
let config = config_iterator.next().expect(
.with_window_builder(Some(create_winit_window_builder(&window_builder)));
let (window, gl_config) = {
crate::profile_scope!("DisplayBuilder::build");
display_builder
.build(
event_loop,
config_template_builder.clone(),
|mut config_iterator| {
let config = config_iterator.next().expect(
"failed to find a matching configuration for creating glutin config",
);
log::debug!(
"using the first config from config picker closure. config: {:?}",
&config
);
config
},
)
.map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))?;
log::debug!(
"using the first config from config picker closure. config: {:?}",
&config
);
config
},
)
.map_err(|e| {
crate::Error::NoGlutinConfigs(config_template_builder.build(), e)
})?
};
let gl_display = gl_config.display();
log::debug!(
@@ -615,10 +638,15 @@ mod glow_integration {
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new()
.with_context_api(glutin::context::ContextApi::Gles(None))
.build(raw_window_handle);
let gl_context = match gl_config
.display()
.create_context(&gl_config, &context_attributes)
{
let gl_context_result = {
crate::profile_scope!("create_context");
gl_config
.display()
.create_context(&gl_config, &context_attributes)
};
let gl_context = match gl_context_result {
Ok(it) => it,
Err(err) => {
log::warn!("failed to create context using default context attributes {context_attributes:?} due to error: {err}");
@@ -675,6 +703,8 @@ mod glow_integration {
///
/// we presently assume that we will
fn on_resume(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
crate::profile_function!();
let values = self
.windows
.values()
@@ -849,6 +879,7 @@ mod glow_integration {
native_options: epi::NativeOptions,
app_creator: epi::AppCreator,
) -> Self {
crate::profile_function!();
Self {
repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())),
app_name: app_name.to_owned(),
@@ -889,6 +920,7 @@ mod glow_integration {
}
let gl = unsafe {
crate::profile_scope!("glow::Context::from_loader_function");
glow::Context::from_loader_function(|s| {
let s = std::ffi::CString::new(s)
.expect("failed to construct C string from string for gl proc address");
@@ -901,6 +933,7 @@ mod glow_integration {
}
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
crate::profile_function!();
let storage = epi_integration::create_storage(
self.native_options
.app_id
@@ -960,14 +993,6 @@ mod glow_integration {
let theme = system_theme.unwrap_or(self.native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals());
gl_window
.window(ViewportId::MAIN)
.read()
.window
.as_ref()
.unwrap()
.read()
.set_ime_allowed(true);
if self.native_options.mouse_passthrough {
gl_window
.window(ViewportId::MAIN)
@@ -1286,7 +1311,10 @@ mod glow_integration {
}
fn save_and_destroy(&mut self) {
crate::profile_function!();
if let Some(running) = self.running.write().take() {
crate::profile_function!();
running.integration.write().save(
running.app.write().as_mut(),
running
@@ -1543,6 +1571,8 @@ mod glow_integration {
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> Result<EventResult> {
crate::profile_function!();
Ok(match event {
winit::event::Event::Resumed => {
// first resume event.
@@ -1632,7 +1662,7 @@ mod glow_integration {
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
let glutin_ctx = &mut *running.glutin_ctx.write();
if physical_size.width > 0 && physical_size.height > 0 {
if 0 < physical_size.width && 0 < physical_size.height {
if let Some(id) = glutin_ctx.window_maps.get(window_id) {
glutin_ctx.resize(*id, *physical_size);
}
@@ -1723,11 +1753,14 @@ mod glow_integration {
EventResult::Wait
}
}
#[cfg(feature = "accesskit")]
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
accesskit_winit::ActionRequestEvent { request, window_id },
)) => {
if let Some(running) = self.running.read().as_ref() {
crate::profile_scope!("on_accesskit_action_request");
let glutin_ctx = running.glutin_ctx.read();
if let Some(viewport_id) = glutin_ctx.window_maps.get(window_id).copied() {
if let Some(viewport) = glutin_ctx.windows.get(&viewport_id).cloned() {
@@ -1764,14 +1797,14 @@ mod glow_integration {
run_and_return(event_loop, glow_eframe)
})
} else {
let event_loop = create_event_loop_builder(&mut native_options).build();
let event_loop = create_event_loop(&mut native_options);
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, glow_eframe);
}
#[cfg(target_os = "ios")]
{
let event_loop = create_event_loop_builder(&mut native_options).build();
let event_loop = create_event_loop(&mut native_options);
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, glow_eframe);
}
@@ -1847,6 +1880,7 @@ mod wgpu_integration {
native_options: epi::NativeOptions,
app_creator: epi::AppCreator,
) -> Self {
crate::profile_function!();
#[cfg(feature = "__screenshot")]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
@@ -1870,10 +1904,15 @@ mod wgpu_integration {
native_options: &NativeOptions,
) -> std::result::Result<(winit::window::Window, ViewportBuilder), winit::error::OsError>
{
crate::profile_function!();
let window_settings = epi_integration::load_window_settings(storage);
let window_builder =
epi_integration::window_builder(event_loop, title, native_options, window_settings);
let window = create_winit_window_builder(&window_builder).build(event_loop)?;
let window = {
crate::profile_scope!("WindowBuilder::build");
create_winit_window_builder(&window_builder).build(event_loop)?
};
epi_integration::apply_native_options_to_window(
&window,
native_options,
@@ -1935,6 +1974,7 @@ mod wgpu_integration {
fn set_window(&mut self, id: ViewportId) -> std::result::Result<(), egui_wgpu::WgpuError> {
if let Some(running) = &mut self.running {
crate::profile_function!();
if let Some(Window { window, .. }) = running.windows.read().get(&id) {
let window = window.clone();
if let Some(win) = &window {
@@ -1966,6 +2006,8 @@ mod wgpu_integration {
window: winit::window::Window,
builder: ViewportBuilder,
) -> std::result::Result<(), egui_wgpu::WgpuError> {
crate::profile_function!();
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let mut painter = egui_wgpu::winit::Painter::new(
self.native_options.wgpu_options.clone(),
@@ -2001,8 +2043,6 @@ mod wgpu_integration {
let theme = system_theme.unwrap_or(self.native_options.default_theme);
integration.egui_ctx.set_visuals(theme.egui_visuals());
window.set_ime_allowed(true);
{
let event_loop_proxy = self.repaint_proxy.clone();
@@ -2026,7 +2066,7 @@ mod wgpu_integration {
let app_creator = std::mem::take(&mut self.app_creator)
.expect("Single-use AppCreator has unexpectedly already been taken");
let mut app = app_creator(&epi::CreationContext {
let cc = epi::CreationContext {
egui_ctx: integration.egui_ctx.clone(),
integration_info: integration.frame.info().clone(),
storage: integration.frame.storage(),
@@ -2035,7 +2075,11 @@ mod wgpu_integration {
wgpu_render_state,
raw_display_handle: window.raw_display_handle(),
raw_window_handle: window.raw_window_handle(),
});
};
let mut app = {
crate::profile_scope!("user_app_creator");
app_creator(&cc)
};
if app.warm_up_enabled() {
integration.warm_up(app.as_mut(), &window, &mut state);
@@ -2189,6 +2233,7 @@ mod wgpu_integration {
fn save_and_destroy(&mut self) {
if let Some(mut running) = self.running.take() {
crate::profile_function!();
if let Some(Window { window, .. }) = running.windows.read().get(&ViewportId::MAIN) {
running
.integration
@@ -2403,7 +2448,9 @@ mod wgpu_integration {
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> Result<EventResult> {
crate::profile_function!();
self.build_windows(event_loop);
Ok(match event {
winit::event::Event::Resumed => {
if let Some(running) = &self.running {
@@ -2484,7 +2531,7 @@ mod wgpu_integration {
if let Some(viewport_id) =
running.windows_id.read().get(window_id).copied()
{
if physical_size.width > 0 && physical_size.height > 0 {
if 0 < physical_size.width && 0 < physical_size.height {
running.painter.write().on_window_resized(
viewport_id,
physical_size.width,
@@ -2600,14 +2647,14 @@ mod wgpu_integration {
run_and_return(event_loop, wgpu_eframe)
})
} else {
let event_loop = create_event_loop_builder(&mut native_options).build();
let event_loop = create_event_loop(&mut native_options);
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe);
}
#[cfg(target_os = "ios")]
{
let event_loop = create_event_loop_builder(&mut native_options).build();
let event_loop = create_event_loop(&mut native_options);
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe);
}
@@ -2628,3 +2675,67 @@ fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Opti
None
}
}
// For the puffin profiler!
#[allow(dead_code)] // Only used for profiling
fn short_event_description(event: &winit::event::Event<'_, UserEvent>) -> &'static str {
use winit::event::{DeviceEvent, Event, StartCause, WindowEvent};
match event {
Event::Suspended => "Event::Suspended",
Event::Resumed => "Event::Resumed",
Event::MainEventsCleared => "Event::MainEventsCleared",
Event::RedrawRequested(_) => "Event::RedrawRequested",
Event::RedrawEventsCleared => "Event::RedrawEventsCleared",
Event::LoopDestroyed => "Event::LoopDestroyed",
Event::UserEvent(user_event) => match user_event {
UserEvent::RequestRepaint { .. } => "UserEvent::RequestRepaint",
#[cfg(feature = "accesskit")]
UserEvent::AccessKitActionRequest(_) => "UserEvent::AccessKitActionRequest",
},
Event::DeviceEvent { event, .. } => match event {
DeviceEvent::Added { .. } => "DeviceEvent::Added",
DeviceEvent::Removed { .. } => "DeviceEvent::Removed",
DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
DeviceEvent::Button { .. } => "DeviceEvent::Button",
DeviceEvent::Key { .. } => "DeviceEvent::Key",
DeviceEvent::Text { .. } => "DeviceEvent::Text",
},
Event::NewEvents(start_cause) => match start_cause {
StartCause::ResumeTimeReached { .. } => "NewEvents::ResumeTimeReached",
StartCause::WaitCancelled { .. } => "NewEvents::WaitCancelled",
StartCause::Poll => "NewEvents::Poll",
StartCause::Init => "NewEvents::Init",
},
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized { .. } => "WindowEvent::Resized",
WindowEvent::Moved { .. } => "WindowEvent::Moved",
WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested",
WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed",
WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled",
WindowEvent::ReceivedCharacter { .. } => "WindowEvent::ReceivedCharacter",
WindowEvent::Focused { .. } => "WindowEvent::Focused",
WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
WindowEvent::Ime { .. } => "WindowEvent::Ime",
WindowEvent::CursorMoved { .. } => "WindowEvent::CursorMoved",
WindowEvent::CursorEntered { .. } => "WindowEvent::CursorEntered",
WindowEvent::CursorLeft { .. } => "WindowEvent::CursorLeft",
WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
WindowEvent::TouchpadMagnify { .. } => "WindowEvent::TouchpadMagnify",
WindowEvent::SmartMagnify { .. } => "WindowEvent::SmartMagnify",
WindowEvent::TouchpadRotate { .. } => "WindowEvent::TouchpadRotate",
WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure",
WindowEvent::AxisMotion { .. } => "WindowEvent::AxisMotion",
WindowEvent::Touch { .. } => "WindowEvent::Touch",
WindowEvent::ScaleFactorChanged { .. } => "WindowEvent::ScaleFactorChanged",
WindowEvent::ThemeChanged { .. } => "WindowEvent::ThemeChanged",
WindowEvent::Occluded { .. } => "WindowEvent::Occluded",
},
}
}

View File

@@ -101,14 +101,13 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> {
// When input lost focus, focus on it again.
// It is useful when user click somewhere outside canvas.
let input_refocus = input.clone();
runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| {
// Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{AGENT_ID}').focus()"
));
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
.unwrap();
let input_refocus = input_refocus.clone();
call_after_delay(std::time::Duration::from_millis(10), move || {
input_refocus.focus().ok();
});
})?;
body.append_child(&input)?;

View File

@@ -203,22 +203,30 @@ pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option<wg
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))]
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))]
puffin::profile_scope!($($arg)*);
};
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
pub(crate) use profile_scope;
#[allow(unused_imports)]
pub(crate) use profiling_scopes::*;

View File

@@ -445,6 +445,13 @@ impl Renderer {
needs_reset = true;
let info = PaintCallbackInfo {
viewport: callback.rect,
clip_rect: *clip_rect,
pixels_per_point,
screen_size_px: size_in_pixels,
};
{
// We're setting a default viewport for the render pass as a
// courtesy for the user, so that they don't have to think about
@@ -455,29 +462,19 @@ impl Renderer {
// viewport during the paint callback, effectively overriding this
// one.
let min = (callback.rect.min.to_vec2() * pixels_per_point).round();
let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
let viewport_px = info.viewport_in_pixels();
render_pass.set_viewport(
min.x,
min.y,
max.x - min.x,
max.y - min.y,
viewport_px.left_px,
viewport_px.top_px,
viewport_px.width_px,
viewport_px.height_px,
0.0,
1.0,
);
}
cbfn.0.paint(
PaintCallbackInfo {
viewport: callback.rect,
clip_rect: *clip_rect,
pixels_per_point,
screen_size_px: size_in_pixels,
},
render_pass,
&self.callback_resources,
);
cbfn.0.paint(info, render_pass, &self.callback_resources);
}
}
}
@@ -605,9 +602,8 @@ impl Renderer {
/// Get the WGPU texture and bind group associated to a texture that has been allocated by egui.
///
/// This could be used by custom paint hooks to render images that have been added through with
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
/// This could be used by custom paint hooks to render images that have been added through
/// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
pub fn texture(
&self,
id: &epaint::TextureId,

View File

@@ -143,6 +143,7 @@ impl Painter {
render_state: &RenderState,
present_mode: wgpu::PresentMode,
) {
crate::profile_function!();
let usage = if surface_state.supports_screenshot {
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST
} else {
@@ -188,6 +189,8 @@ impl Painter {
viewport_id: ViewportId,
window: Option<&winit::window::Window>,
) -> Result<(), crate::WgpuError> {
crate::profile_function!();
if let Some(window) = window {
let size = window.inner_size();
if self.surfaces.get(&viewport_id).is_none() {
@@ -269,6 +272,7 @@ impl Painter {
width_in_pixels: u32,
height_in_pixels: u32,
) {
crate::profile_function!();
let render_state = self.render_state.as_ref().unwrap();
let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
@@ -329,6 +333,8 @@ impl Painter {
width_in_pixels: u32,
height_in_pixels: u32,
) {
crate::profile_function!();
if self.surfaces.get(&viewport_id).is_some() {
self.resize_and_generate_depth_texture_view_and_msaa_view(
viewport_id,

View File

@@ -83,6 +83,8 @@ pub struct State {
#[cfg(feature = "accesskit")]
accesskit: Option<accesskit_winit::Adapter>,
allow_ime: bool,
}
impl State {
@@ -110,6 +112,8 @@ impl State {
#[cfg(feature = "accesskit")]
accesskit: None,
allow_ime: false,
}
}
@@ -227,6 +231,8 @@ impl State {
egui_ctx: &egui::Context,
event: &winit::event::WindowEvent<'_>,
) -> EventResponse {
crate::profile_function!();
use winit::event::WindowEvent;
match event {
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
@@ -706,6 +712,12 @@ impl State {
self.clipboard.set(copied_text);
}
let allow_ime = text_cursor_pos.is_some();
if self.allow_ime != allow_ime {
self.allow_ime = allow_ime;
window.set_ime_allowed(allow_ime);
}
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
}
@@ -1343,29 +1355,33 @@ pub fn changes_between_builders(
}
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
#[allow(unused_macros)]
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_function!($($arg)*);
};
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[allow(unused_imports)]
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
#[allow(unused_macros)]
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
puffin::profile_scope!($($arg)*);
};
}
#[allow(unused_imports)]
pub(crate) use profile_scope;
pub(crate) use profiling_scopes::*;
use winit::{
dpi::{LogicalPosition, LogicalSize},
window::{CursorGrabMode, WindowButtons, WindowLevel},

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -1,10 +1,13 @@
#![warn(missing_docs)] // Let's keep `Context` well-documented.
use std::borrow::Cow;
use std::sync::Arc;
use crate::load::Bytes;
use crate::load::SizedTexture;
use crate::{
animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState,
input_state::*, layers::GraphicLayers, memory::Options, os::OperatingSystem,
input_state::*, layers::GraphicLayers, load::Loaders, memory::Options, os::OperatingSystem,
output::FullOutput, util::IdTypeMap, TextureHandle, ViewportCommand, *,
};
use ahash::HashMap;
@@ -228,7 +231,7 @@ struct ContextImpl {
#[cfg(feature = "accesskit")]
accesskit_node_classes: accesskit::NodeClassSet,
loaders: load::Loaders,
loaders: Arc<Loaders>,
}
impl ContextImpl {
@@ -1293,17 +1296,24 @@ impl Context {
self.options(|opt| opt.style.clone())
}
/// The [`Style`] used by all new windows, panels etc.
///
/// You can also use [`Ui::style_mut`] to change the style of a single [`Ui`].
/// Mutate the [`Style`] used by all subsequent windows, panels etc.
///
/// Example:
/// ```
/// # let mut ctx = egui::Context::default();
/// let mut style: egui::Style = (*ctx.style()).clone();
/// style.spacing.item_spacing = egui::vec2(10.0, 20.0);
/// ctx.set_style(style);
/// ctx.style_mut(|style| {
/// style.spacing.item_spacing = egui::vec2(10.0, 20.0);
/// });
/// ```
pub fn style_mut(&self, mutate_style: impl FnOnce(&mut Style)) {
self.options_mut(|opt| mutate_style(std::sync::Arc::make_mut(&mut opt.style)));
}
/// The [`Style`] used by all new windows, panels etc.
///
/// You can also change this using [`Self::style_mut]`
///
/// You can use [`Ui::style_mut`] to change the style of a single [`Ui`].
pub fn set_style(&self, style: impl Into<Arc<Style>>) {
self.options_mut(|opt| opt.style = style.into());
}
@@ -1365,9 +1375,16 @@ impl Context {
/// Allocate a texture.
///
/// In order to display an image you must convert it to a texture using this function.
/// This is for advanced users.
/// Most users should use [`crate::Ui::image`] or [`Self::try_load_texture`]
/// instead.
///
/// Make sure to only call this once for each image, i.e. NOT in your main GUI code.
/// In order to display an image you must convert it to a texture using this function.
/// The function will hand over the image data to the egui backend, which will
/// upload it to the GPU.
///
/// ⚠️ Make sure to only call this ONCE for each image, i.e. NOT in your main GUI code.
/// The call is NOT immediate safe.
///
/// The given name can be useful for later debugging, and will be visible if you call [`Self::texture_ui`].
///
@@ -1390,12 +1407,12 @@ impl Context {
/// });
///
/// // Show the image:
/// ui.image(texture, texture.size_vec2());
/// ui.image((texture.id(), texture.size_vec2()));
/// }
/// }
/// ```
///
/// Se also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::ImageButton`].
/// See also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::Image`].
pub fn load_texture(
&self,
name: impl Into<String>,
@@ -1533,7 +1550,7 @@ impl Context {
nodes,
tree: Some(accesskit::Tree::new(root_id)),
focus: has_focus.then(|| {
let focus_id = self.memory(|mem| mem.interaction.focus.id);
let focus_id = self.memory(|mem| mem.focus());
focus_id.map_or(root_id, |id| id.accesskit_id())
}),
});
@@ -2070,14 +2087,15 @@ impl Context {
let mut size = vec2(w as f32, h as f32);
size *= (max_preview_size.x / size.x).min(1.0);
size *= (max_preview_size.y / size.y).min(1.0);
ui.image(texture_id, size).on_hover_ui(|ui| {
// show larger on hover
let max_size = 0.5 * ui.ctx().screen_rect().size();
let mut size = vec2(w as f32, h as f32);
size *= max_size.x / size.x.max(max_size.x);
size *= max_size.y / size.y.max(max_size.y);
ui.image(texture_id, size);
});
ui.image(SizedTexture::new(texture_id, size))
.on_hover_ui(|ui| {
// show larger on hover
let max_size = 0.5 * ui.ctx().screen_rect().size();
let mut size = vec2(w as f32, h as f32);
size *= max_size.x / size.x.max(max_size.x);
size *= max_size.y / size.y.max(max_size.y);
ui.image(SizedTexture::new(texture_id, size));
});
ui.label(format!("{w} x {h}"));
ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6));
@@ -2290,60 +2308,93 @@ impl Context {
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::image`] later to load the bytes as an image.
///
/// 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));
/// By convention, the `uri` should start with `bytes://`.
/// Following that convention will lead to better error messages.
pub fn include_bytes(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
self.loaders().include.insert(uri, bytes);
}
/// Append an entry onto the chain of bytes loaders.
/// Returns `true` if the chain of bytes, image, or texture loaders
/// contains a loader with the given `id`.
pub fn is_loader_installed(&self, id: &str) -> bool {
let loaders = self.loaders();
loaders.bytes.lock().iter().any(|l| l.id() == id)
|| loaders.image.lock().iter().any(|l| l.id() == id)
|| loaders.texture.lock().iter().any(|l| l.id() == id)
}
/// Add a new bytes loader.
///
/// It will be tried first, before any already installed 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));
self.loaders().bytes.lock().push(loader);
}
/// Append an entry onto the chain of image loaders.
/// Add a new image loader.
///
/// It will be tried first, before any already installed 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));
self.loaders().image.lock().push(loader);
}
/// Append an entry onto the chain of texture loaders.
/// Add a new texture loader.
///
/// It will be tried first, before any already installed 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));
self.loaders().texture.lock().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 _;
use load::BytesLoader as _;
ctx.loaders.include.forget(uri);
crate::profile_function!();
for loader in &ctx.loaders.bytes {
loader.forget(uri);
}
let loaders = self.loaders();
for loader in &ctx.loaders.image {
loader.forget(uri);
}
loaders.include.forget(uri);
for loader in loaders.bytes.lock().iter() {
loader.forget(uri);
}
for loader in loaders.image.lock().iter() {
loader.forget(uri);
}
for loader in loaders.texture.lock().iter() {
loader.forget(uri);
}
}
for loader in &ctx.loaders.texture {
loader.forget(uri);
}
});
/// Release all memory and textures related to images used in [`Ui::image`] or [`Image`].
///
/// If you attempt to load any images again, they will be reloaded from scratch.
pub fn forget_all_images(&self) {
use load::BytesLoader as _;
crate::profile_function!();
let loaders = self.loaders();
loaders.include.forget_all();
for loader in loaders.bytes.lock().iter() {
loader.forget_all();
}
for loader in loaders.image.lock().iter() {
loader.forget_all();
}
for loader in loaders.texture.lock().iter() {
loader.forget_all();
}
}
/// Try loading the bytes from the given uri using any available bytes loaders.
@@ -2358,21 +2409,27 @@ impl Context {
/// # 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.
/// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
///
/// ⚠ May deadlock if called from within a `BytesLoader`!
///
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
/// [custom]: crate::load::LoadError::Loading
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,
}
}
crate::profile_function!();
Err(load::LoadError::NotSupported)
})
let loaders = self.loaders();
let bytes_loaders = loaders.bytes.lock();
// Try most recently added loaders first (hence `.rev()`)
for loader in bytes_loaders.iter().rev() {
match loader.load(self, uri) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
}
}
Err(load::LoadError::NoMatchingBytesLoader)
}
/// Try loading the image from the given uri using any available image loaders.
@@ -2386,22 +2443,33 @@ impl Context {
///
/// # Errors
/// This may fail with:
/// - [`LoadError::NoImageLoaders`][no_image_loaders] if tbere are no registered image loaders.
/// - [`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.
/// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
///
/// ⚠ May deadlock if called from within an `ImageLoader`!
///
/// [no_image_loaders]: crate::load::LoadError::NoImageLoaders
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
/// [custom]: crate::load::LoadError::Loading
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,
}
}
crate::profile_function!();
Err(load::LoadError::NotSupported)
})
let loaders = self.loaders();
let image_loaders = loaders.image.lock();
if image_loaders.is_empty() {
return Err(load::LoadError::NoImageLoaders);
}
// 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,
result => return result,
}
}
Err(load::LoadError::NoMatchingImageLoader)
}
/// Try loading the texture from the given uri using any available texture loaders.
@@ -2416,26 +2484,38 @@ impl Context {
/// # 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.
/// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
///
/// ⚠ May deadlock if called from within a `TextureLoader`!
///
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
/// [custom]: crate::load::LoadError::Loading
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,
}
}
crate::profile_function!();
Err(load::LoadError::NotSupported)
})
let loaders = self.loaders();
let texture_loaders = loaders.texture.lock();
// Try most recently added loaders first (hence `.rev()`)
for loader in texture_loaders.iter().rev() {
match loader.load(self, uri, texture_options, size_hint) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
}
}
Err(load::LoadError::NoMatchingTextureLoader)
}
/// The loaders of bytes, images, and textures.
pub fn loaders(&self) -> Arc<Loaders> {
crate::profile_function!();
self.read(|this| this.loaders.clone())
}
}

View File

@@ -1080,3 +1080,62 @@ impl From<u32> for TouchId {
Self(id as u64)
}
}
// ----------------------------------------------------------------------------
// TODO(emilk): generalize this to a proper event filter.
/// Controls which events that a focused widget will have exclusive access to.
///
/// Currently this only controls a few special keyboard events,
/// but in the future this `struct` should be extended into a full callback thing.
///
/// Any events not covered by the filter are given to the widget, but are not exclusive.
#[derive(Clone, Copy, Debug)]
pub struct EventFilter {
/// If `true`, pressing tab will act on the widget,
/// and NOT move focus away from the focused widget.
///
/// Default: `false`
pub tab: bool,
/// If `true`, pressing arrows will act on the widget,
/// and NOT move focus away from the focused widget.
///
/// Default: `false`
pub arrows: bool,
/// If `true`, pressing escape will act on the widget,
/// and NOT surrender focus from the focused widget.
///
/// Default: `false`
pub escape: bool,
}
#[allow(clippy::derivable_impls)] // let's be explicit
impl Default for EventFilter {
fn default() -> Self {
Self {
tab: false,
arrows: false,
escape: false,
}
}
}
impl EventFilter {
pub fn matches(&self, event: &Event) -> bool {
if let Event::Key { key, .. } = event {
match key {
crate::Key::Tab => self.tab,
crate::Key::ArrowUp
| crate::Key::ArrowRight
| crate::Key::ArrowDown
| crate::Key::ArrowLeft => self.arrows,
crate::Key::Escape => self.escape,
_ => true,
}
} else {
true
}
}
}

View File

@@ -490,6 +490,15 @@ impl InputState {
pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize {
self.accesskit_action_requests(id, action).count()
}
/// Get all events that matches the given filter.
pub fn filtered_events(&self, filter: &EventFilter) -> Vec<Event> {
self.events
.iter()
.filter(|event| filter.matches(event))
.cloned()
.collect()
}
}
// ----------------------------------------------------------------------------

View File

@@ -84,7 +84,7 @@
//! ui.separator();
//!
//! # let my_image = egui::TextureId::default();
//! ui.image(my_image, [640.0, 480.0]);
//! ui.image((my_image, egui::Vec2::new(640.0, 480.0)));
//!
//! ui.collapsing("Click to see what is hidden!", |ui| {
//! ui.label("Not much, as it turns out");
@@ -107,9 +107,9 @@
//!
//! Most likely you are using an existing `egui` backend/integration such as [`eframe`](https://docs.rs/eframe), [`bevy_egui`](https://docs.rs/bevy_egui),
//! or [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad),
//! but if you want to integrate `egui` into a new game engine, this is the section for you.
//! but if you want to integrate `egui` into a new game engine or graphics backend, this is the section for you.
//!
//! To write your own integration for egui you need to do this:
//! You need to collect [`RawInput`] and handle [`FullOutput`]. The basic structure is this:
//!
//! ``` no_run
//! # fn handle_platform_output(_: egui::PlatformOutput) {}
@@ -135,6 +135,31 @@
//! }
//! ```
//!
//! For a reference OpenGL renderer, see [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs).
//!
//!
//! ### Debugging your renderer
//!
//! #### Things look jagged
//!
//! * Turn off backface culling.
//!
//! #### My text is blurry
//!
//! * Make sure you set the proper `pixels_per_point` in the input to egui.
//! * Make sure the texture sampler is not off by half a pixel. Try nearest-neighbor sampler to check.
//!
//! #### My windows are too transparent or too dark
//!
//! * egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`.
//! * Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`).
//! * egui prefers linear color spaces for all blending so:
//! * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`).
//! * Otherwise: remember to decode gamma in the fragment shader.
//! * Decode the gamma of the incoming vertex colors in your vertex shader.
//! * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`).
//! * Otherwise: gamma-encode the colors before you write them again.
//!
//!
//! # Understanding immediate mode
//!
@@ -401,6 +426,34 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
// ----------------------------------------------------------------------------
/// Include an image in the binary.
///
/// This is a wrapper over `include_bytes!`, and behaves in the same way.
///
/// It produces an [`ImageSource`] which can be used directly in [`Ui::image`] or [`Image::new`]:
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.image(egui::include_image!("../assets/ferris.png"));
/// ui.add(
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
/// .rounding(egui::Rounding::same(6.0))
/// );
///
/// let image_source: egui::ImageSource = egui::include_image!("../assets/ferris.png");
/// assert_eq!(image_source.uri(), Some("bytes://../assets/ferris.png"));
/// # });
/// ```
#[macro_export]
macro_rules! include_image {
($path: literal) => {
$crate::ImageSource::Bytes(
::std::borrow::Cow::Borrowed(concat!("bytes://", $path)), // uri
$crate::load::Bytes::Static(include_bytes!($path)),
)
};
}
/// Create a [`Hyperlink`](crate::Hyperlink) to the current [`file!()`] (and line) on Github
///
/// ```
@@ -605,8 +658,8 @@ mod profiling_scopes {
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
@@ -615,12 +668,13 @@ mod profiling_scopes {
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
#[allow(unused_imports)]
pub(crate) use profiling_scopes::*;

View File

@@ -1,8 +1,12 @@
//! Types and traits related to image loading.
//! # 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).
//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/)
//! will get you up and running quickly with its reasonable default implementations of the traits described below.
//!
//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature.
//! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html)
//! in your app's setup code.
//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`].
//!
//! ## Loading process
//!
@@ -14,13 +18,13 @@
//! 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", ...)
//! ui.image("file://image.png")
//! └► Context::try_load_texture
//! └► TextureLoader::load
//! └► Context::try_load_image
//! └► ImageLoader::load
//! └► Context::try_load_bytes
//! └► BytesLoader::load
//! ```
//!
//! As each layer attempts to load the URI, it first asks the layer below it
@@ -48,28 +52,65 @@
//! For example, a loader may determine that it doesn't support loading a specific URI
//! if the protocol does not match what it expects.
mod bytes_loader;
mod texture_loader;
use crate::Context;
use ahash::HashMap;
use epaint::mutex::Mutex;
use epaint::util::FloatOrd;
use epaint::util::OrderedFloat;
use epaint::TextureHandle;
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
use std::borrow::Cow;
use std::fmt::Debug;
use std::ops::Deref;
use std::{error::Error as StdError, fmt::Display, sync::Arc};
pub use self::bytes_loader::DefaultBytesLoader;
pub use self::texture_loader::DefaultTextureLoader;
/// Represents a failed attempt at loading an image.
#[derive(Clone, Debug)]
pub enum LoadError {
/// This loader does not support this protocol or image format.
/// Programmer error: There are no image loaders installed.
NoImageLoaders,
/// A specific loader does not support this schema, protocol or image format.
NotSupported,
/// A custom error message (e.g. "File not found: foo.png").
Custom(String),
/// Programmer error: Failed to find the bytes for this image because
/// there was no [`BytesLoader`] supporting the schema.
NoMatchingBytesLoader,
/// Programmer error: Failed to parse the bytes as an image because
/// there was no [`ImageLoader`] supporting the schema.
NoMatchingImageLoader,
/// Programmer error: no matching [`TextureLoader`].
/// Because of the [`DefaultTextureLoader`], this error should never happen.
NoMatchingTextureLoader,
/// Runtime error: Loading was attempted, but failed (e.g. "File not found").
Loading(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),
Self::NoImageLoaders => f.write_str(
"No image loaders are installed. If you're trying to load some images \
for the first time, follow the steps outlined in https://docs.rs/egui/latest/egui/load/index.html"),
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::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"),
Self::NotSupported => f.write_str("Iagge schema or URI not supported by this loader"),
Self::Loading(message) => f.write_str(message),
}
}
}
@@ -85,11 +126,10 @@ pub type Result<T, E = LoadError> = std::result::Result<T, E>;
/// All variants will preserve the original aspect ratio.
///
/// Similar to `usvg::FitTo`.
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SizeHint {
/// Keep original size.
#[default]
Original,
/// Scale original size by some factor.
Scale(OrderedFloat<f32>),
/// Scale to width.
Width(u32),
@@ -101,22 +141,36 @@ pub enum SizeHint {
Size(u32, u32),
}
impl Default for SizeHint {
fn default() -> Self {
Self::Scale(1.0.ord())
}
}
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];
/// Represents a byte buffer.
///
/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`.
#[derive(Clone)]
pub enum Bytes {
Static(&'static [u8]),
Shared(Arc<[u8]>),
}
impl Debug for Bytes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(),
Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(),
}
}
}
impl From<&'static [u8]> for Bytes {
#[inline]
fn from(value: &'static [u8]) -> Self {
@@ -124,6 +178,13 @@ impl From<&'static [u8]> for Bytes {
}
}
impl<const N: usize> From<&'static [u8; N]> for Bytes {
#[inline]
fn from(value: &'static [u8; N]) -> Self {
Bytes::Static(value)
}
}
impl From<Arc<[u8]>> for Bytes {
#[inline]
fn from(value: Arc<[u8]>) -> Self {
@@ -131,6 +192,13 @@ impl From<Arc<[u8]>> for Bytes {
}
}
impl From<Vec<u8>> for Bytes {
#[inline]
fn from(value: Vec<u8>) -> Self {
Bytes::Shared(value.into())
}
}
impl AsRef<[u8]> for Bytes {
#[inline]
fn as_ref(&self) -> &[u8] {
@@ -150,27 +218,60 @@ impl Deref for Bytes {
}
}
/// Represents bytes which are currently being loaded.
///
/// This is similar to [`std::task::Poll`], but the `Pending` variant
/// contains an optional `size`, which may be used during layout to
/// pre-allocate space the image.
#[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>,
size: Option<Vec2>,
},
/// Bytes are loaded.
Ready {
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Size>,
size: Option<Vec2>,
/// File contents, e.g. the contents of a `.png`.
bytes: Bytes,
/// Mime type of the content, e.g. `image/png`.
///
/// Set if known (e.g. from `Content-Type` HTTP header).
mime: Option<String>,
},
}
/// Used to get a unique ID when implementing one of the loader traits: [`BytesLoader::id`], [`ImageLoader::id`], and [`TextureLoader::id`].
///
/// This just expands to `module_path!()` concatenated with the given type name.
#[macro_export]
macro_rules! generate_loader_id {
($ty:ident) => {
concat!(module_path!(), "::", stringify!($ty))
};
}
pub use crate::generate_loader_id;
pub type BytesLoadResult = Result<BytesPoll>;
/// Represents a loader capable of loading raw unstructured bytes from somewhere,
/// e.g. from disk or network.
///
/// It should also provide any subsequent loaders a hint for what the bytes may
/// represent using [`BytesPoll::Ready::mime`], if it can be inferred.
///
/// Implementations are expected to cache at least each `URI`.
pub trait BytesLoader {
/// Unique ID of this loader.
///
/// To reduce the chance of collisions, use [`generate_loader_id`] for this.
fn id(&self) -> &str;
/// Try loading the bytes from the given uri.
///
/// Implementations should call `ctx.request_repaint` to wake up the ui
@@ -182,7 +283,7 @@ pub trait BytesLoader {
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
/// - [`LoadError::Custom`] if the loading process failed.
/// - [`LoadError::Loading`] if the loading process failed.
fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult;
/// Forget the given `uri`.
@@ -191,6 +292,12 @@ pub trait BytesLoader {
/// so that it may be fully reloaded.
fn forget(&self, uri: &str);
/// Forget all URIs ever given to this loader.
///
/// If the loader caches any URIs, the entire cache should be cleared,
/// so that all of them may be fully reloaded.
fn forget_all(&self);
/// 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) {
@@ -201,12 +308,17 @@ pub trait BytesLoader {
fn byte_size(&self) -> usize;
}
/// Represents an image which is currently being loaded.
///
/// This is similar to [`std::task::Poll`], but the `Pending` variant
/// contains an optional `size`, which may be used during layout to
/// pre-allocate space the image.
#[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>,
size: Option<Vec2>,
},
/// Image is loaded.
@@ -215,7 +327,18 @@ pub enum ImagePoll {
pub type ImageLoadResult = Result<ImagePoll>;
/// An `ImageLoader` decodes raw bytes into a [`ColorImage`].
///
/// Implementations are expected to cache at least each `URI`.
pub trait ImageLoader {
/// Unique ID of this loader.
///
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
///
/// For example: `concat!(module_path!(), "::MyLoader")`
/// for `my_crate::my_loader::MyLoader`.
fn id(&self) -> &str;
/// Try loading the image from the given uri.
///
/// Implementations should call `ctx.request_repaint` to wake up the ui
@@ -227,7 +350,7 @@ pub trait ImageLoader {
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
/// - [`LoadError::Custom`] if the loading process failed.
/// - [`LoadError::Loading`] if the loading process failed.
fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult;
/// Forget the given `uri`.
@@ -236,6 +359,12 @@ pub trait ImageLoader {
/// so that it may be fully reloaded.
fn forget(&self, uri: &str);
/// Forget all URIs ever given to this loader.
///
/// If the loader caches any URIs, the entire cache should be cleared,
/// so that all of them may be fully reloaded.
fn forget_all(&self);
/// 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) {
@@ -247,36 +376,91 @@ pub trait ImageLoader {
}
/// A texture with a known size.
#[derive(Clone)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SizedTexture {
pub id: TextureId,
pub size: Size,
pub size: Vec2,
}
impl SizedTexture {
/// Create a [`SizedTexture`] from a texture `id` with a specific `size`.
pub fn new(id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
Self {
id: id.into(),
size: size.into(),
}
}
/// Fetch the [id][`SizedTexture::id`] and [size][`SizedTexture::size`] from a [`TextureHandle`].
pub fn from_handle(handle: &TextureHandle) -> Self {
let size = handle.size();
Self {
id: handle.id(),
size: handle.size(),
size: Vec2::new(size[0] as f32, size[1] as f32),
}
}
}
#[derive(Clone)]
impl From<(TextureId, Vec2)> for SizedTexture {
#[inline]
fn from((id, size): (TextureId, Vec2)) -> Self {
Self { id, size }
}
}
impl<'a> From<&'a TextureHandle> for SizedTexture {
fn from(handle: &'a TextureHandle) -> Self {
Self::from_handle(handle)
}
}
/// Represents a texture is currently being loaded.
///
/// This is similar to [`std::task::Poll`], but the `Pending` variant
/// contains an optional `size`, which may be used during layout to
/// pre-allocate space the image.
#[derive(Clone, Copy)]
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>,
size: Option<Vec2>,
},
/// Texture is loaded.
Ready { texture: SizedTexture },
}
impl TexturePoll {
pub fn size(self) -> Option<Vec2> {
match self {
TexturePoll::Pending { size } => size,
TexturePoll::Ready { texture } => Some(texture.size),
}
}
}
pub type TextureLoadResult = Result<TexturePoll>;
/// A `TextureLoader` uploads a [`ColorImage`] to the GPU, returning a [`SizedTexture`].
///
/// `egui` comes with an implementation that uses [`Context::load_texture`],
/// which just asks the egui backend to upload the image to the GPU.
///
/// You can implement this trait if you do your own uploading of images to the GPU.
/// For instance, you can use this to refer to textures in a game engine that egui
/// doesn't otherwise know about.
///
/// Implementations are expected to cache each combination of `(URI, TextureOptions)`.
pub trait TextureLoader {
/// Unique ID of this loader.
///
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
///
/// For example: `concat!(module_path!(), "::MyLoader")`
/// for `my_crate::my_loader::MyLoader`.
fn id(&self) -> &str;
/// Try loading the texture from the given uri.
///
/// Implementations should call `ctx.request_repaint` to wake up the ui
@@ -288,7 +472,7 @@ pub trait TextureLoader {
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
/// - [`LoadError::Custom`] if the loading process failed.
/// - [`LoadError::Loading`] if the loading process failed.
fn load(
&self,
ctx: &Context,
@@ -303,6 +487,12 @@ pub trait TextureLoader {
/// so that it may be fully reloaded.
fn forget(&self, uri: &str);
/// Forget all URIs ever given to this loader.
///
/// If the loader caches any URIs, the entire cache should be cleared,
/// so that all of them may be fully reloaded.
fn forget_all(&self);
/// 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) {
@@ -313,104 +503,27 @@ pub trait TextureLoader {
fn byte_size(&self) -> usize;
}
#[derive(Default)]
pub(crate) struct DefaultBytesLoader {
cache: Mutex<HashMap<&'static str, Bytes>>,
}
type BytesLoaderImpl = Arc<dyn BytesLoader + Send + Sync + 'static>;
type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
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 {
#[derive(Clone)]
/// The loaders of bytes, images, and textures.
pub 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>>,
pub bytes: Mutex<Vec<BytesLoaderImpl>>,
pub image: Mutex<Vec<ImageLoaderImpl>>,
pub texture: Mutex<Vec<TextureLoaderImpl>>,
}
impl Default for Loaders {
fn default() -> Self {
let include = Arc::new(DefaultBytesLoader::default());
Self {
bytes: vec![include.clone()],
image: Vec::new(),
bytes: Mutex::new(vec![include.clone()]),
image: Mutex::new(Vec::new()),
// By default we only include `DefaultTextureLoader`.
texture: vec![Arc::new(DefaultTextureLoader::default())],
texture: Mutex::new(vec![Arc::new(DefaultTextureLoader::default())]),
include,
}
}

View File

@@ -0,0 +1,69 @@
use super::*;
/// Maps URI:s to [`Bytes`], e.g. found with `include_bytes!`.
///
/// By convention, the URI:s should be prefixed with `bytes://`.
#[derive(Default)]
pub struct DefaultBytesLoader {
cache: Mutex<HashMap<Cow<'static, str>, Bytes>>,
}
impl DefaultBytesLoader {
pub fn insert(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
self.cache
.lock()
.entry(uri.into())
.or_insert_with_key(|_uri| {
let bytes: Bytes = bytes.into();
#[cfg(feature = "log")]
log::trace!("loaded {} bytes for uri {_uri:?}", bytes.len());
bytes
});
}
}
impl BytesLoader for DefaultBytesLoader {
fn id(&self) -> &str {
generate_loader_id!(DefaultBytesLoader)
}
fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
// We accept uri:s that don't start with `bytes://` too… for now.
match self.cache.lock().get(uri).cloned() {
Some(bytes) => Ok(BytesPoll::Ready {
size: None,
bytes,
mime: None,
}),
None => {
if uri.starts_with("bytes://") {
Err(LoadError::Loading(
"Bytes not found. Did you forget to call Context::include_bytes?".into(),
))
} else {
Err(LoadError::NotSupported)
}
}
}
}
fn forget(&self, uri: &str) {
#[cfg(feature = "log")]
log::trace!("forget {uri:?}");
let _ = self.cache.lock().remove(uri);
}
fn forget_all(&self) {
#[cfg(feature = "log")]
log::trace!("forget all");
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache.lock().values().map(|bytes| bytes.len()).sum()
}
}

View File

@@ -0,0 +1,60 @@
use super::*;
#[derive(Default)]
pub struct DefaultTextureLoader {
cache: Mutex<HashMap<(String, TextureOptions), TextureHandle>>,
}
impl TextureLoader for DefaultTextureLoader {
fn id(&self) -> &str {
crate::generate_loader_id!(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) {
#[cfg(feature = "log")]
log::trace!("forget {uri:?}");
self.cache.lock().retain(|(u, _), _| u != uri);
}
fn forget_all(&self) {
#[cfg(feature = "log")]
log::trace!("forget all");
self.cache.lock().clear();
}
fn end_frame(&self, _: usize) {}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|texture| texture.byte_size())
.sum()
}
}

View File

@@ -1,4 +1,6 @@
use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style, ViewportId};
use crate::{
area, window, EventFilter, Id, IdMap, InputState, LayerId, Pos2, Rect, Style, ViewportId,
};
use ahash::HashMap;
use epaint::{emath::Rangef, vec2, Vec2};
@@ -236,7 +238,7 @@ pub(crate) struct Interaction {
#[derive(Clone, Debug, Default)]
pub(crate) struct Focus {
/// The widget with keyboard focus (i.e. a text input field).
pub(crate) id: Option<Id>,
focused_widget: Option<FocusWidget>,
/// What had keyboard focus previous frame?
id_previous_frame: Option<Id>,
@@ -254,9 +256,6 @@ pub(crate) struct Focus {
/// The last widget interested in focus.
last_interested: Option<Id>,
/// If `true`, pressing tab will NOT move focus away from the current widget.
is_focus_locked: bool,
/// Set when looking for widget with navigational keys like arrows, tab, shift+tab
focus_direction: FocusDirection,
@@ -264,6 +263,22 @@ pub(crate) struct Focus {
focus_widgets_cache: IdMap<Rect>,
}
/// The widget with focus.
#[derive(Clone, Copy, Debug)]
struct FocusWidget {
pub id: Id,
pub filter: EventFilter,
}
impl FocusWidget {
pub fn new(id: Id) -> Self {
Self {
id,
filter: Default::default(),
}
}
}
impl Interaction {
/// Are we currently clicking or dragging an egui widget?
pub fn is_using_pointer(&self) -> bool {
@@ -295,14 +310,15 @@ impl Interaction {
impl Focus {
/// Which widget currently has keyboard focus?
pub fn focused(&self) -> Option<Id> {
self.id
self.focused_widget.as_ref().map(|w| w.id)
}
fn begin_frame(&mut self, new_input: &crate::data::input::RawInput) {
self.id_previous_frame = self.id;
self.id_previous_frame = self.focused();
if let Some(id) = self.id_next_frame.take() {
self.id = Some(id);
self.focused_widget = Some(FocusWidget::new(id));
}
let event_filter = self.focused_widget.map(|w| w.filter).unwrap_or_default();
#[cfg(feature = "accesskit")]
{
@@ -312,37 +328,35 @@ impl Focus {
self.focus_direction = FocusDirection::None;
for event in &new_input.events {
if let crate::Event::Key {
key,
pressed: true,
modifiers,
..
} = event
{
if let Some(cardinality) = match key {
crate::Key::ArrowUp => Some(FocusDirection::Up),
crate::Key::ArrowRight => Some(FocusDirection::Right),
crate::Key::ArrowDown => Some(FocusDirection::Down),
crate::Key::ArrowLeft => Some(FocusDirection::Left),
crate::Key::Tab => {
if !self.is_focus_locked {
if !event_filter.matches(event) {
if let crate::Event::Key {
key,
pressed: true,
modifiers,
..
} = event
{
if let Some(cardinality) = match key {
crate::Key::ArrowUp => Some(FocusDirection::Up),
crate::Key::ArrowRight => Some(FocusDirection::Right),
crate::Key::ArrowDown => Some(FocusDirection::Down),
crate::Key::ArrowLeft => Some(FocusDirection::Left),
crate::Key::Tab => {
if modifiers.shift {
Some(FocusDirection::Previous)
} else {
Some(FocusDirection::Next)
}
} else {
None
}
crate::Key::Escape => {
self.focused_widget = None;
Some(FocusDirection::None)
}
_ => None,
} {
self.focus_direction = cardinality;
}
crate::Key::Escape => {
self.id = None;
self.is_focus_locked = false;
Some(FocusDirection::None)
}
_ => None,
} {
self.focus_direction = cardinality;
}
}
@@ -363,17 +377,17 @@ impl Focus {
pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
if self.focus_direction.is_cardinal() {
if let Some(found_widget) = self.find_widget_in_direction(used_ids) {
self.id = Some(found_widget);
self.focused_widget = Some(FocusWidget::new(found_widget));
}
}
if let Some(id) = self.id {
if let Some(focused_widget) = self.focused_widget {
// Allow calling `request_focus` one frame and not using it until next frame
let recently_gained_focus = self.id_previous_frame != Some(id);
let recently_gained_focus = self.id_previous_frame != Some(focused_widget.id);
if !recently_gained_focus && !used_ids.contains_key(&id) {
if !recently_gained_focus && !used_ids.contains_key(&focused_widget.id) {
// Dead-mans-switch: the widget with focus has disappeared!
self.id = None;
self.focused_widget = None;
}
}
}
@@ -386,7 +400,7 @@ impl Focus {
#[cfg(feature = "accesskit")]
{
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
self.id = Some(id);
self.focused_widget = Some(FocusWidget::new(id));
self.id_requested_by_accesskit = None;
self.give_to_next = false;
self.reset_focus();
@@ -399,23 +413,23 @@ impl Focus {
.or_insert(Rect::EVERYTHING);
if self.give_to_next && !self.had_focus_last_frame(id) {
self.id = Some(id);
self.focused_widget = Some(FocusWidget::new(id));
self.give_to_next = false;
} else if self.id == Some(id) {
if self.focus_direction == FocusDirection::Next && !self.is_focus_locked {
self.id = None;
} else if self.focused() == Some(id) {
if self.focus_direction == FocusDirection::Next {
self.focused_widget = None;
self.give_to_next = true;
self.reset_focus();
} else if self.focus_direction == FocusDirection::Previous && !self.is_focus_locked {
} else if self.focus_direction == FocusDirection::Previous {
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
self.reset_focus();
}
} else if self.focus_direction == FocusDirection::Next
&& self.id.is_none()
&& self.focused_widget.is_none()
&& !self.give_to_next
{
// nothing has focus and the user pressed tab - give focus to the first widgets that wants it:
self.id = Some(id);
self.focused_widget = Some(FocusWidget::new(id));
self.reset_focus();
}
@@ -441,7 +455,7 @@ impl Focus {
}
}
let Some(focus_id) = self.id else {
let Some(current_focused) = self.focused_widget else {
return None;
};
@@ -466,7 +480,7 @@ impl Focus {
}
});
let Some(current_rect) = self.focus_widgets_cache.get(&focus_id) else {
let Some(current_rect) = self.focus_widgets_cache.get(&current_focused.id) else {
return None;
};
@@ -474,7 +488,7 @@ impl Focus {
let mut best_id = None;
for (candidate_id, candidate_rect) in &self.focus_widgets_cache {
if Some(*candidate_id) == self.id {
if *candidate_id == current_focused.id {
continue;
}
@@ -605,46 +619,58 @@ impl Memory {
/// from the window and back.
#[inline(always)]
pub fn has_focus(&self, id: Id) -> bool {
self.interaction.focus.id == Some(id)
self.interaction.focus.focused() == Some(id)
}
/// Which widget has keyboard focus?
pub fn focus(&self) -> Option<Id> {
self.interaction.focus.id
self.interaction.focus.focused()
}
/// Prevent keyboard focus from moving away from this widget even if users presses the tab key.
/// Set an event filter for a widget.
///
/// This allows you to control whether the widget will loose focus
/// when the user presses tab, arrow keys, or escape.
///
/// You must first give focus to the widget before calling this.
pub fn lock_focus(&mut self, id: Id, lock_focus: bool) {
pub fn set_focus_lock_filter(&mut self, id: Id, event_filter: EventFilter) {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked = lock_focus;
if let Some(focused) = &mut self.interaction.focus.focused_widget {
if focused.id == id {
focused.filter = event_filter;
}
}
}
}
/// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key.
pub fn has_lock_focus(&self, id: Id) -> bool {
if self.had_focus_last_frame(id) && self.has_focus(id) {
self.interaction.focus.is_focus_locked
} else {
false
}
/// Set an event filter for a widget.
///
/// You must first give focus to the widget before calling this.
#[deprecated = "Use set_focus_lock_filter instead"]
pub fn lock_focus(&mut self, id: Id, lock_focus: bool) {
self.set_focus_lock_filter(
id,
EventFilter {
tab: lock_focus,
arrows: lock_focus,
escape: false,
},
);
}
/// Give keyboard focus to a specific widget.
/// See also [`crate::Response::request_focus`].
#[inline(always)]
pub fn request_focus(&mut self, id: Id) {
self.interaction.focus.id = Some(id);
self.interaction.focus.is_focus_locked = false;
self.interaction.focus.focused_widget = Some(FocusWidget::new(id));
}
/// Surrender keyboard focus for a specific widget.
/// See also [`crate::Response::surrender_focus`].
#[inline(always)]
pub fn surrender_focus(&mut self, id: Id) {
if self.interaction.focus.id == Some(id) {
self.interaction.focus.id = None;
self.interaction.focus.is_focus_locked = false;
if self.interaction.focus.focused() == Some(id) {
self.interaction.focus.focused_widget = None;
}
}
@@ -663,7 +689,7 @@ impl Memory {
/// Stop editing of active [`TextEdit`](crate::TextEdit) (if any).
#[inline(always)]
pub fn stop_text_input(&mut self) {
self.interaction.focus.id = None;
self.interaction.focus.focused_widget = None;
}
#[inline(always)]

View File

@@ -111,7 +111,7 @@ pub fn menu_button<R>(
/// Returns `None` if the menu is not open.
pub fn menu_image_button<R>(
ui: &mut Ui,
image_button: ImageButton,
image_button: ImageButton<'_>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_image_impl(ui, image_button, Box::new(add_contents))
@@ -201,7 +201,7 @@ fn stationary_menu_impl<'c, R>(
/// Responds to primary clicks.
fn stationary_menu_image_impl<'c, R>(
ui: &mut Ui,
image_button: ImageButton,
image_button: ImageButton<'_>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let bar_id = ui.id();

View File

@@ -350,6 +350,18 @@ impl Painter {
/// unless you want to crop or flip the image.
///
/// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image.
///
/// Usually it is easier to use [`crate::Image::paint_at`] instead:
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0));
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
/// .rounding(5.0)
/// .tint(egui::Color32::LIGHT_BLUE)
/// .paint_at(ui, rect);
/// # });
/// ```
pub fn image(&self, texture_id: epaint::TextureId, rect: Rect, uv: Rect, tint: Color32) {
self.add(Shape::image(texture_id, rect, uv, tint));
}

View File

@@ -2,7 +2,9 @@
#![allow(clippy::if_same_then_else)]
use crate::{ecolor::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText};
use crate::{
ecolor::*, emath::*, ComboBox, CursorIcon, FontFamily, FontId, Response, RichText, WidgetText,
};
use epaint::{Rounding, Shadow, Stroke};
use std::collections::BTreeMap;
@@ -544,6 +546,16 @@ pub struct Visuals {
///
/// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`].
pub slider_trailing_fill: bool,
/// Should the cursor change when the user hovers over an interactive/clickable item?
///
/// This is consistent with a lot of browser-based applications (vscode, github
/// all turn your cursor into [`CursorIcon::PointingHand`] when a button is
/// hovered) but it is inconsistent with native UI toolkits.
pub interact_cursor: Option<CursorIcon>,
/// Show a spinner when loading an image.
pub image_loading_spinners: bool,
}
impl Visuals {
@@ -806,6 +818,10 @@ impl Visuals {
striped: false,
slider_trailing_fill: false,
interact_cursor: None,
image_loading_spinners: true,
}
}
@@ -1376,6 +1392,9 @@ impl Visuals {
striped,
slider_trailing_fill,
interact_cursor,
image_loading_spinners,
} = self;
ui.collapsing("Background Colors", |ui| {
@@ -1441,6 +1460,19 @@ impl Visuals {
ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
ComboBox::from_label("Interact Cursor")
.selected_text(format!("{interact_cursor:?}"))
.show_ui(ui, |ui| {
ui.selectable_value(interact_cursor, None, "None");
for icon in CursorIcon::ALL {
ui.selectable_value(interact_cursor, Some(icon), format!("{icon:?}"));
}
});
ui.checkbox(image_loading_spinners, "Image loading spinners")
.on_hover_text("Show a spinner when an Image is loading");
ui.vertical_centered(|ui| reset_button(ui, self));
}
}

View File

@@ -1558,56 +1558,34 @@ impl Ui {
response
}
/// Show an image here with the given size.
///
/// In order to display an image you must first acquire a [`TextureHandle`].
/// This is best done with [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) or [`Context::load_texture`].
///
/// ```
/// struct MyImage {
/// texture: Option<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture(
/// "my-image",
/// egui::ColorImage::example(),
/// Default::default()
/// )
/// });
///
/// // Show the image:
/// ui.image(texture, texture.size_vec2());
/// }
/// }
/// ```
///
/// See also [`crate::Image`] and [`crate::ImageButton`].
#[inline]
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 easiest way to do this is via [`egui_extras::install_image_loaders`](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");
/// ui.image("https://picsum.photos/480");
/// ui.image("file://assets/ferris.png");
/// ui.image(egui::include_image!("../assets/ferris.png"));
/// ui.add(
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
/// .rounding(egui::Rounding::same(6.0))
/// );
/// # });
/// ```
///
/// See also [`crate::Image2`] and [`crate::ImageSource`].
/// Using [`include_image`] is often the most ergonomic, and the path
/// will be resolved at compile-time and embedded in the binary.
/// When using a "file://" url on the other hand, you need to make sure
/// the files can be found in the right spot at runtime!
///
/// See also [`crate::Image`], [`crate::ImageSource`].
#[inline]
pub fn image2<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
Image2::new(source.into()).ui(self)
pub fn image<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
Image::new(source).ui(self)
}
}
@@ -1710,7 +1688,7 @@ impl Ui {
/// # });
/// ```
///
/// Se also [`Self::scope`].
/// See also [`Self::scope`].
pub fn group<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
crate::Frame::group(self.style()).show(self, add_contents)
}
@@ -2206,15 +2184,9 @@ impl Ui {
/// If called from within a menu this will instead create a button for a sub-menu.
///
/// ```ignore
/// use egui_extras;
/// let img = egui::include_image!("../assets/ferris.png");
///
/// let img = egui_extras::RetainedImage::from_svg_bytes_with_size(
/// "rss",
/// include_bytes!("rss.svg"),
/// egui_extras::image::FitTo::Size(24, 24),
/// );
///
/// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| {
/// ui.menu_image_button(img, |ui| {
/// ui.menu_button("My sub-menu", |ui| {
/// if ui.button("Close the menu").clicked() {
/// ui.close_menu();
@@ -2225,16 +2197,15 @@ impl Ui {
///
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
#[inline]
pub fn menu_image_button<R>(
pub fn menu_image_button<'a, R>(
&mut self,
texture_id: TextureId,
image_size: impl Into<Vec2>,
image: impl Into<Image<'a>>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
if let Some(menu_state) = self.menu_state.clone() {
menu::submenu_button(self, menu_state, String::new(), add_contents)
} else {
menu::menu_image_button(self, ImageButton::new(texture_id, image_size), add_contents)
menu::menu_image_button(self, ImageButton::new(image), add_contents)
}
}
}

View File

@@ -269,12 +269,66 @@ impl RichText {
fonts.row_height(&font_id)
}
/// Append to an existing [`LayoutJob`]
///
/// Note that the color of the [`RichText`] must be set, or may default to an undesirable color.
///
/// ### Example
/// ```
/// use egui::{Style, RichText, text::LayoutJob, Color32, FontSelection, Align};
///
/// let style = Style::default();
/// let mut layout_job = LayoutJob::default();
/// RichText::new("Normal")
/// .color(style.visuals.text_color())
/// .append_to(
/// &mut layout_job,
/// &style,
/// FontSelection::Default,
/// Align::Center,
/// );
/// RichText::new("Large and underlined")
/// .color(style.visuals.text_color())
/// .size(20.0)
/// .underline()
/// .append_to(
/// &mut layout_job,
/// &style,
/// FontSelection::Default,
/// Align::Center,
/// );
/// ```
pub fn append_to(
self,
layout_job: &mut LayoutJob,
style: &Style,
fallback_font: FontSelection,
default_valign: Align,
) {
let (text, format) = self.into_text_and_format(style, fallback_font, default_valign);
layout_job.append(&text, 0.0, format);
}
fn into_text_job(
self,
style: &Style,
fallback_font: FontSelection,
default_valign: Align,
) -> WidgetTextJob {
let job_has_color = self.get_text_color(&style.visuals).is_some();
let (text, text_format) = self.into_text_and_format(style, fallback_font, default_valign);
let job = LayoutJob::single_section(text, text_format);
WidgetTextJob { job, job_has_color }
}
fn into_text_and_format(
self,
style: &Style,
fallback_font: FontSelection,
default_valign: Align,
) -> (String, crate::text::TextFormat) {
let text_color = self.get_text_color(&style.visuals);
let Self {
@@ -295,7 +349,6 @@ impl RichText {
raised,
} = self;
let job_has_color = text_color.is_some();
let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
let text_color = text_color.unwrap_or(crate::Color32::TEMPORARY_COLOR);
@@ -336,20 +389,20 @@ impl RichText {
default_valign
};
let text_format = crate::text::TextFormat {
font_id,
extra_letter_spacing,
line_height,
color: text_color,
background: background_color,
italics,
underline,
strikethrough,
valign,
};
let job = LayoutJob::single_section(text, text_format);
WidgetTextJob { job, job_has_color }
(
text,
crate::text::TextFormat {
font_id,
extra_letter_spacing,
line_height,
color: text_color,
background: background_color,
italics,
underline,
strikethrough,
valign,
},
)
}
fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {

View File

@@ -19,8 +19,9 @@ use crate::*;
/// # });
/// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Button {
text: WidgetText,
pub struct Button<'a> {
image: Option<Image<'a>>,
text: Option<WidgetText>,
shortcut_text: WidgetText,
wrap: Option<bool>,
@@ -32,13 +33,30 @@ pub struct Button {
frame: Option<bool>,
min_size: Vec2,
rounding: Option<Rounding>,
image: Option<widgets::Image>,
selected: bool,
}
impl Button {
impl<'a> Button<'a> {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self::opt_image_and_text(None, Some(text.into()))
}
/// Creates a button with an image. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)]
pub fn image(image: impl Into<Image<'a>>) -> Self {
Self::opt_image_and_text(Some(image.into()), None)
}
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)]
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
Self::opt_image_and_text(Some(image.into()), Some(text.into()))
}
pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
Self {
text: text.into(),
text,
image,
shortcut_text: Default::default(),
wrap: None,
fill: None,
@@ -48,20 +66,7 @@ impl Button {
frame: None,
min_size: Vec2::ZERO,
rounding: None,
image: None,
}
}
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)]
pub fn image_and_text(
texture_id: TextureId,
image_size: impl Into<Vec2>,
text: impl Into<WidgetText>,
) -> Self {
Self {
image: Some(widgets::Image::new(texture_id, image_size)),
..Self::new(text)
selected: false,
}
}
@@ -96,7 +101,9 @@ impl Button {
/// Make this a small button, suitable for embedding into text.
pub fn small(mut self) -> Self {
self.text = self.text.text_style(TextStyle::Body);
if let Some(text) = self.text {
self.text = Some(text.text_style(TextStyle::Body));
}
self.small = true;
self
}
@@ -135,12 +142,19 @@ impl Button {
self.shortcut_text = shortcut_text.into();
self
}
/// If `true`, mark this button as "selected".
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl Widget for Button {
impl Widget for Button<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let Button {
text,
image,
shortcut_text,
wrap,
fill,
@@ -150,32 +164,58 @@ impl Widget for Button {
frame,
min_size,
rounding,
image,
selected,
} = self;
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
let mut button_padding = ui.spacing().button_padding;
let mut button_padding = if frame {
ui.spacing().button_padding
} else {
Vec2::ZERO
};
if small {
button_padding.y = 0.0;
}
let space_available_for_image = if let Some(text) = &text {
let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style()));
Vec2::splat(font_height) // Reasonable?
} else {
ui.available_size() - 2.0 * button_padding
};
let image_size = if let Some(image) = &image {
image
.load_and_calc_size(ui, space_available_for_image)
.unwrap_or(space_available_for_image)
} else {
Vec2::ZERO
};
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
if let Some(image) = image {
text_wrap_width -= image.size().x + ui.spacing().icon_spacing;
if image.is_some() {
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
}
if !shortcut_text.is_empty() {
text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap).
}
let text = text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button);
let text = text.map(|text| text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button));
let shortcut_text = (!shortcut_text.is_empty())
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
let mut desired_size = text.size();
if let Some(image) = image {
desired_size.x += image.size().x + ui.spacing().icon_spacing;
desired_size.y = desired_size.y.max(image.size().y);
let mut desired_size = Vec2::ZERO;
if image.is_some() {
desired_size.x += image_size.x;
desired_size.y = desired_size.y.max(image_size.y);
}
if image.is_some() && text.is_some() {
desired_size.x += ui.spacing().icon_spacing;
}
if let Some(text) = &text {
desired_size.x += text.size().x;
desired_size.y = desired_size.y.max(text.size().y);
}
if let Some(shortcut_text) = &shortcut_text {
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
@@ -187,32 +227,82 @@ impl Widget for Button {
}
desired_size = desired_size.at_least(min_size);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text()));
let (rect, mut response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| {
if let Some(text) = &text {
WidgetInfo::labeled(WidgetType::Button, text.text())
} else {
WidgetInfo::new(WidgetType::Button)
}
});
if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response);
if frame {
let fill = fill.unwrap_or(visuals.weak_bg_fill);
let stroke = stroke.unwrap_or(visuals.bg_stroke);
let rounding = rounding.unwrap_or(visuals.rounding);
ui.painter()
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
}
let text_pos = if let Some(image) = image {
let icon_spacing = ui.spacing().icon_spacing;
pos2(
rect.min.x + button_padding.x + image.size().x + icon_spacing,
rect.center().y - 0.5 * text.size().y,
let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected {
let selection = ui.visuals().selection;
(
Vec2::ZERO,
Rounding::ZERO,
selection.bg_fill,
selection.stroke,
)
} else if frame {
let expansion = Vec2::splat(visuals.expansion);
(
expansion,
visuals.rounding,
visuals.weak_bg_fill,
visuals.bg_stroke,
)
} else {
ui.layout()
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min
Default::default()
};
text.paint_with_visuals(ui.painter(), text_pos, visuals);
let frame_rounding = rounding.unwrap_or(frame_rounding);
let frame_fill = fill.unwrap_or(frame_fill);
let frame_stroke = stroke.unwrap_or(frame_stroke);
ui.painter().rect(
rect.expand2(frame_expansion),
frame_rounding,
frame_fill,
frame_stroke,
);
let mut cursor_x = rect.min.x + button_padding.x;
if let Some(image) = &image {
let image_rect = Rect::from_min_size(
pos2(cursor_x, rect.center().y - 0.5 - (image_size.y / 2.0)),
image_size,
);
cursor_x += image_size.x;
let tlr = image.load_for_size(ui.ctx(), image_size);
widgets::image::paint_texture_load_result(
ui,
&tlr,
image_rect,
image.show_loading_spinner,
image.image_options(),
);
response =
widgets::image::texture_load_result_response(image.source(), &tlr, response);
}
if image.is_some() && text.is_some() {
cursor_x += ui.spacing().icon_spacing;
}
if let Some(text) = text {
let text_pos = if image.is_some() || shortcut_text.is_some() {
pos2(cursor_x, rect.center().y - 0.5 * text.size().y)
} else {
// Make sure button text is centered if within a centered layout
ui.layout()
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min
};
text.paint_with_visuals(ui.painter(), text_pos, visuals);
}
if let Some(shortcut_text) = shortcut_text {
let shortcut_text_pos = pos2(
@@ -225,16 +315,11 @@ impl Widget for Button {
ui.visuals().weak_text_color(),
);
}
}
if let Some(image) = image {
let image_rect = Rect::from_min_size(
pos2(
rect.min.x + button_padding.x,
rect.center().y - 0.5 - (image.size().y / 2.0),
),
image.size(),
);
image.paint_at(ui, image_rect);
if let Some(cursor) = ui.visuals().interact_cursor {
if response.hovered {
ui.ctx().set_cursor_icon(cursor);
}
}
@@ -462,17 +547,17 @@ impl Widget for RadioButton {
/// A clickable image within a frame.
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Clone, Debug)]
pub struct ImageButton {
image: widgets::Image,
pub struct ImageButton<'a> {
image: Image<'a>,
sense: Sense,
frame: bool,
selected: bool,
}
impl ImageButton {
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
impl<'a> ImageButton<'a> {
pub fn new(image: impl Into<Image<'a>>) -> Self {
Self {
image: widgets::Image::new(texture_id, size),
image: image.into(),
sense: Sense::click(),
frame: true,
selected: false,
@@ -511,27 +596,28 @@ impl ImageButton {
}
}
impl Widget for ImageButton {
impl<'a> Widget for ImageButton<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let Self {
image,
sense,
frame,
selected,
} = self;
let padding = if frame {
let padding = if self.frame {
// so we can see that it is a button:
Vec2::splat(ui.spacing().button_padding.x)
} else {
Vec2::ZERO
};
let padded_size = image.size() + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, sense);
let available_size_for_image = ui.available_size() - 2.0 * padding;
let tlr = self.image.load_for_size(ui.ctx(), available_size_for_image);
let original_image_size = tlr.as_ref().ok().and_then(|t| t.size());
let image_size = self
.image
.calc_size(available_size_for_image, original_image_size);
let padded_size = image_size + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
if ui.is_rect_visible(rect) {
let (expansion, rounding, fill, stroke) = if selected {
let (expansion, rounding, fill, stroke) = if self.selected {
let selection = ui.visuals().selection;
(
Vec2::ZERO,
@@ -539,7 +625,7 @@ impl Widget for ImageButton {
selection.bg_fill,
selection.stroke,
)
} else if frame {
} else if self.frame {
let visuals = ui.style().interact(&response);
let expansion = Vec2::splat(visuals.expansion);
(
@@ -552,23 +638,25 @@ impl Widget for ImageButton {
Default::default()
};
let image = image.rounding(rounding); // apply rounding to the image
// Draw frame background (for transparent images):
ui.painter()
.rect_filled(rect.expand2(expansion), rounding, fill);
let image_rect = ui
.layout()
.align_size_within_rect(image.size(), rect.shrink2(padding));
.align_size_within_rect(image_size, rect.shrink2(padding));
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
image.paint_at(ui, image_rect);
let image_options = ImageOptions {
rounding, // apply rounding to the image
..self.image.image_options().clone()
};
widgets::image::paint_texture_load_result(ui, &tlr, image_rect, None, &image_options);
// Draw frame outline:
ui.painter()
.rect_stroke(rect.expand2(expansion), rounding, stroke);
}
response
widgets::image::texture_load_result_response(self.image.source(), &tlr, response)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ pub mod text_edit;
pub use button::*;
pub use drag_value::DragValue;
pub use hyperlink::*;
pub use image::{Image, Image2, ImageSource};
pub use image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource};
pub use label::*;
pub use progress_bar::ProgressBar;
pub use selected_label::SelectableLabel;

View File

@@ -570,6 +570,16 @@ impl<'a> Slider<'a> {
let mut increment = 0usize;
if response.has_focus() {
ui.ctx().memory_mut(|m| {
m.set_focus_lock_filter(
response.id,
EventFilter {
arrows: true, // pressing arrows should not move focus to next widget
..Default::default()
},
);
});
let (dec_key, inc_key) = match self.orientation {
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
// Note that this is for moving the slider position,
@@ -595,13 +605,14 @@ impl<'a> Slider<'a> {
let kb_step = increment as f32 - decrement as f32;
if kb_step != 0.0 {
let ui_point_per_step = 1.0; // move this many ui points for each kb_step
let prev_value = self.get_value();
let prev_position = self.position_from_value(prev_value, position_range);
let new_position = prev_position + kb_step;
let new_position = prev_position + ui_point_per_step * kb_step;
let new_value = match self.step {
Some(step) => prev_value + (kb_step as f64 * step),
None if self.smart_aim => {
let aim_radius = ui.input(|i| i.aim_radius());
let aim_radius = 0.49 * ui_point_per_step; // Chosen so we don't include `prev_value` in the search.
emath::smart_aim::best_in_range_f64(
self.value_from_position(new_position - aim_radius, position_range),
self.value_from_position(new_position + aim_radius, position_range),

View File

@@ -1,4 +1,4 @@
use epaint::{emath::lerp, vec2, Color32, Pos2, Shape, Stroke};
use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke};
use crate::{Response, Sense, Ui, Widget};
@@ -31,21 +31,15 @@ impl Spinner {
self.color = Some(color.into());
self
}
}
impl Widget for Spinner {
fn ui(self, ui: &mut Ui) -> Response {
let size = self
.size
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
let color = self
.color
.unwrap_or_else(|| ui.visuals().strong_text_color());
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
/// Paint the spinner in the given rectangle.
pub fn paint_at(&self, ui: &Ui, rect: Rect) {
if ui.is_rect_visible(rect) {
ui.ctx().request_repaint();
ui.ctx().request_repaint(); // because it is animated
let color = self
.color
.unwrap_or_else(|| ui.visuals().strong_text_color());
let radius = (rect.height() / 2.0) - 2.0;
let n_points = 20;
let time = ui.input(|i| i.time);
@@ -61,6 +55,16 @@ impl Widget for Spinner {
ui.painter()
.add(Shape::line(points, Stroke::new(3.0, color)));
}
}
}
impl Widget for Spinner {
fn ui(self, ui: &mut Ui) -> Response {
let size = self
.size
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
self.paint_at(ui, rect);
response
}

View File

@@ -65,7 +65,7 @@ pub struct TextEdit<'t> {
interactive: bool,
desired_width: Option<f32>,
desired_height_rows: usize,
lock_focus: bool,
event_filter: EventFilter,
cursor_at_end: bool,
min_size: Vec2,
align: Align2,
@@ -115,7 +115,11 @@ impl<'t> TextEdit<'t> {
interactive: true,
desired_width: None,
desired_height_rows: 4,
lock_focus: false,
event_filter: EventFilter {
arrows: true, // moving the cursor is really important
tab: false, // tab is used to change focus, not to insert a tab character
..Default::default()
},
cursor_at_end: true,
min_size: Vec2::ZERO,
align: Align2::LEFT_TOP,
@@ -127,7 +131,7 @@ impl<'t> TextEdit<'t> {
/// Build a [`TextEdit`] focused on code editing.
/// By default it comes with:
/// - monospaced font
/// - focus lock
/// - focus lock (tab will insert a tab character instead of moving focus)
pub fn code_editor(self) -> Self {
self.font(TextStyle::Monospace).lock_focus(true)
}
@@ -266,8 +270,8 @@ impl<'t> TextEdit<'t> {
///
/// When `true`, the widget will keep the focus and pressing TAB
/// will insert the `'\t'` character.
pub fn lock_focus(mut self, b: bool) -> Self {
self.lock_focus = b;
pub fn lock_focus(mut self, tab_will_indent: bool) -> Self {
self.event_filter.tab = tab_will_indent;
self
}
@@ -352,7 +356,9 @@ impl<'t> TextEdit<'t> {
let margin = self.margin;
let max_rect = ui.available_rect_before_wrap().shrink2(margin);
let mut content_ui = ui.child_ui(max_rect, *ui.layout());
let mut output = self.show_content(&mut content_ui);
let id = output.response.id;
let frame_rect = output.response.rect.expand2(margin);
ui.allocate_space(frame_rect.size());
@@ -413,7 +419,7 @@ impl<'t> TextEdit<'t> {
interactive,
desired_width,
desired_height_rows,
lock_focus,
event_filter,
cursor_at_end,
min_size,
align,
@@ -569,7 +575,7 @@ impl<'t> TextEdit<'t> {
let mut cursor_range = None;
let prev_cursor_range = state.cursor_range(&galley);
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.lock_focus(id, lock_focus));
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
let default_cursor_range = if cursor_at_end {
CursorRange::one(galley.end())
@@ -589,6 +595,7 @@ impl<'t> TextEdit<'t> {
password,
default_cursor_range,
char_limit,
event_filter,
);
if changed {
@@ -880,6 +887,7 @@ fn events(
password: bool,
default_cursor_range: CursorRange,
char_limit: usize,
event_filter: EventFilter,
) -> (bool, CursorRange) {
let mut cursor_range = state.cursor_range(galley).unwrap_or(default_cursor_range);
@@ -898,7 +906,7 @@ fn events(
let mut any_change = false;
let events = ui.input(|i| i.events.clone()); // avoid dead-lock by cloning. TODO(emilk): optimize
let events = ui.input(|i| i.filtered_events(&event_filter));
for event in &events {
let did_mutate_text = match event {
Event::Copy => {
@@ -946,19 +954,15 @@ fn events(
pressed: true,
modifiers,
..
} => {
if multiline && ui.memory(|mem| mem.has_lock_focus(id)) {
let mut ccursor = delete_selected(text, &cursor_range);
if modifiers.shift {
// TODO(emilk): support removing indentation over a selection?
decrease_indentation(&mut ccursor, text);
} else {
insert_text(&mut ccursor, text, "\t", char_limit);
}
Some(CCursorRange::one(ccursor))
} if multiline => {
let mut ccursor = delete_selected(text, &cursor_range);
if modifiers.shift {
// TODO(emilk): support removing indentation over a selection?
decrease_indentation(&mut ccursor, text);
} else {
None
insert_text(&mut ccursor, text, "\t", char_limit);
}
Some(CCursorRange::one(ccursor))
}
Event::Key {
key: Key::Enter,

View File

@@ -1,4 +1,4 @@
use std::ops::Range;
use std::{borrow::Cow, ops::Range};
/// Trait constraining what types [`crate::TextEdit`] may use as
/// an underlying buffer.
@@ -100,6 +100,36 @@ impl TextBuffer for String {
}
}
impl<'a> TextBuffer for Cow<'a, str> {
fn is_mutable(&self) -> bool {
true
}
fn as_str(&self) -> &str {
self.as_ref()
}
fn insert_text(&mut self, text: &str, char_index: usize) -> usize {
<String as TextBuffer>::insert_text(self.to_mut(), text, char_index)
}
fn delete_char_range(&mut self, char_range: Range<usize>) {
<String as TextBuffer>::delete_char_range(self.to_mut(), char_range);
}
fn clear(&mut self) {
<String as TextBuffer>::clear(self.to_mut());
}
fn replace(&mut self, text: &str) {
*self = Cow::Owned(text.to_owned());
}
fn take(&mut self) -> String {
std::mem::take(self).into_owned()
}
}
/// Immutable view of a `&str`!
impl<'a> TextBuffer for &'a str {
fn is_mutable(&self) -> bool {

View File

@@ -19,15 +19,15 @@ crate-type = ["cdylib", "rlib"]
default = ["glow", "persistence"]
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
image_viewer = ["image", "egui_extras/all_loaders", "rfd"]
persistence = ["eframe/persistence", "egui/persistence", "serde"]
web_screen_reader = ["eframe/web_screen_reader"] # experimental
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
syntect = ["egui_demo_lib/syntect"]
glow = ["eframe/glow"]
wgpu = ["eframe/wgpu", "bytemuck"]
[dependencies]
chrono = { version = "0.4", default-features = false, features = [
"js-sys",
@@ -36,6 +36,7 @@ chrono = { version = "0.4", default-features = false, features = [
eframe = { version = "0.22.0", path = "../eframe", default-features = false }
egui = { version = "0.22.0", path = "../egui", features = [
"extra_debug_asserts",
"log",
] }
egui_demo_lib = { version = "0.22.0", path = "../egui_demo_lib", features = [
"chrono",
@@ -45,9 +46,10 @@ 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", features = [
"log",
egui_extras = { version = "0.22.0", path = "../egui_extras", features = [
"image",
] }
rfd = { version = "0.11", optional = true }
# feature "http":
ehttp = { version = "0.3.0", optional = true }

View File

@@ -1,4 +1,4 @@
use egui_extras::RetainedImage;
use egui::Image;
use poll_promise::Promise;
struct Resource {
@@ -8,7 +8,7 @@ struct Resource {
text: Option<String>,
/// If set, the response was an image.
image: Option<RetainedImage>,
image: Option<Image<'static>>,
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
colored_text: Option<ColoredText>,
@@ -17,21 +17,27 @@ struct Resource {
impl Resource {
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
let content_type = response.content_type().unwrap_or_default();
let image = if content_type.starts_with("image/") {
RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
if content_type.starts_with("image/") {
ctx.include_bytes(response.url.clone(), response.bytes.clone());
let image = Image::from_uri(response.url.clone());
Self {
response,
text: None,
colored_text: None,
image: Some(image),
}
} else {
None
};
let text = response.text();
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
let text = text.map(|text| text.to_owned());
let text = response.text();
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
let text = text.map(|text| text.to_owned());
Self {
response,
text,
image,
colored_text,
Self {
response,
text,
colored_text,
image: None,
}
}
}
}
@@ -63,6 +69,7 @@ impl eframe::App for HttpApp {
});
egui::CentralPanel::default().show(ctx, |ui| {
let prev_url = self.url.clone();
let trigger_fetch = ui_url(ui, frame, &mut self.url);
ui.horizontal_wrapped(|ui| {
@@ -77,6 +84,7 @@ impl eframe::App for HttpApp {
let (sender, promise) = Promise::new();
let request = ehttp::Request::get(&self.url);
ehttp::fetch(request, move |response| {
ctx.forget_image(&prev_url);
ctx.request_repaint(); // wake up UI thread
let resource = response.map(|response| Resource::from_response(&ctx, response));
sender.send(resource);
@@ -193,9 +201,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
}
if let Some(image) = image {
let mut size = image.size_vec2();
size *= (ui.available_width() / size.x).min(1.0);
image.show_size(ui, size);
ui.add(image.clone());
} else if let Some(colored_text) = colored_text {
colored_text.ui(ui);
} else if let Some(text) = &text {
@@ -217,25 +223,19 @@ fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
// ----------------------------------------------------------------------------
// Syntax highlighting:
#[cfg(feature = "syntect")]
fn syntax_highlighting(
ctx: &egui::Context,
response: &ehttp::Response,
text: &str,
) -> Option<ColoredText> {
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
let extension = extension_and_rest.get(0)?;
let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style());
Some(ColoredText(crate::syntax_highlighting::highlight(
let extension = extension_and_rest.first()?;
let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(&ctx.style());
Some(ColoredText(egui_extras::syntax_highlighting::highlight(
ctx, &theme, text, extension,
)))
}
#[cfg(not(feature = "syntect"))]
fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option<ColoredText> {
None
}
struct ColoredText(egui::text::LayoutJob);
impl ColoredText {

View File

@@ -0,0 +1,214 @@
use egui::emath::Rot2;
use egui::panel::Side;
use egui::panel::TopBottomSide;
use egui::ImageFit;
use egui::Slider;
use egui::Vec2;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ImageViewer {
current_uri: String,
uri_edit_text: String,
image_options: egui::ImageOptions,
chosen_fit: ChosenFit,
fit: ImageFit,
maintain_aspect_ratio: bool,
max_size: Vec2,
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
enum ChosenFit {
ExactSize,
Fraction,
OriginalSize,
}
impl ChosenFit {
fn as_str(&self) -> &'static str {
match self {
ChosenFit::ExactSize => "exact size",
ChosenFit::Fraction => "fraction",
ChosenFit::OriginalSize => "original size",
}
}
}
impl Default for ImageViewer {
fn default() -> Self {
Self {
current_uri: "https://picsum.photos/seed/1.759706314/1024".to_owned(),
uri_edit_text: "https://picsum.photos/seed/1.759706314/1024".to_owned(),
image_options: egui::ImageOptions::default(),
chosen_fit: ChosenFit::Fraction,
fit: ImageFit::Fraction(Vec2::splat(1.0)),
maintain_aspect_ratio: true,
max_size: Vec2::splat(2048.0),
}
}
}
impl eframe::App for ImageViewer {
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
ui.horizontal_centered(|ui| {
ui.label("URI:");
ui.text_edit_singleline(&mut self.uri_edit_text);
if ui.small_button("").clicked() {
ctx.forget_image(&self.current_uri);
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
self.current_uri = self.uri_edit_text.clone();
};
#[cfg(not(target_arch = "wasm32"))]
if ui.button("file…").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_file() {
self.uri_edit_text = format!("file://{}", path.display());
self.current_uri = self.uri_edit_text.clone();
}
}
});
});
egui::SidePanel::new(Side::Left, "controls").show(ctx, |ui| {
// uv
ui.label("UV");
ui.add(Slider::new(&mut self.image_options.uv.min.x, 0.0..=1.0).text("min x"));
ui.add(Slider::new(&mut self.image_options.uv.min.y, 0.0..=1.0).text("min y"));
ui.add(Slider::new(&mut self.image_options.uv.max.x, 0.0..=1.0).text("max x"));
ui.add(Slider::new(&mut self.image_options.uv.max.y, 0.0..=1.0).text("max y"));
// rotation
ui.add_space(2.0);
let had_rotation = self.image_options.rotation.is_some();
let mut has_rotation = had_rotation;
ui.checkbox(&mut has_rotation, "Rotation");
match (had_rotation, has_rotation) {
(true, false) => self.image_options.rotation = None,
(false, true) => {
self.image_options.rotation =
Some((Rot2::from_angle(0.0), Vec2::new(0.5, 0.5)));
}
(true, true) | (false, false) => {}
}
if let Some((rot, origin)) = self.image_options.rotation.as_mut() {
let mut angle = rot.angle();
ui.label("angle");
ui.drag_angle(&mut angle);
*rot = Rot2::from_angle(angle);
ui.add(Slider::new(&mut origin.x, 0.0..=1.0).text("origin x"));
ui.add(Slider::new(&mut origin.y, 0.0..=1.0).text("origin y"));
}
// bg_fill
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.color_edit_button_srgba(&mut self.image_options.bg_fill);
ui.label("Background color");
});
// tint
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.color_edit_button_srgba(&mut self.image_options.tint);
ui.label("Tint");
});
// fit
ui.add_space(10.0);
ui.label(
"The chosen fit will determine how the image tries to fill the available space",
);
egui::ComboBox::from_label("Fit")
.selected_text(self.chosen_fit.as_str())
.show_ui(ui, |ui| {
ui.selectable_value(
&mut self.chosen_fit,
ChosenFit::ExactSize,
ChosenFit::ExactSize.as_str(),
);
ui.selectable_value(
&mut self.chosen_fit,
ChosenFit::Fraction,
ChosenFit::Fraction.as_str(),
);
ui.selectable_value(
&mut self.chosen_fit,
ChosenFit::OriginalSize,
ChosenFit::OriginalSize.as_str(),
);
});
match self.chosen_fit {
ChosenFit::ExactSize => {
if !matches!(self.fit, ImageFit::Exact(_)) {
self.fit = ImageFit::Exact(Vec2::splat(128.0));
}
let ImageFit::Exact(size) = &mut self.fit else {
unreachable!()
};
ui.add(Slider::new(&mut size.x, 0.0..=2048.0).text("width"));
ui.add(Slider::new(&mut size.y, 0.0..=2048.0).text("height"));
}
ChosenFit::Fraction => {
if !matches!(self.fit, ImageFit::Fraction(_)) {
self.fit = ImageFit::Fraction(Vec2::splat(1.0));
}
let ImageFit::Fraction(fract) = &mut self.fit else {
unreachable!()
};
ui.add(Slider::new(&mut fract.x, 0.0..=1.0).text("width"));
ui.add(Slider::new(&mut fract.y, 0.0..=1.0).text("height"));
}
ChosenFit::OriginalSize => {
if !matches!(self.fit, ImageFit::Original { .. }) {
self.fit = ImageFit::Original { scale: 1.0 };
}
let ImageFit::Original { scale } = &mut self.fit else {
unreachable!()
};
ui.add(Slider::new(scale, 0.1..=4.0).text("scale"));
}
}
// max size
ui.add_space(5.0);
ui.label("The calculated size will not exceed the maximum size");
ui.add(Slider::new(&mut self.max_size.x, 0.0..=2048.0).text("width"));
ui.add(Slider::new(&mut self.max_size.y, 0.0..=2048.0).text("height"));
// aspect ratio
ui.add_space(5.0);
ui.label("Aspect ratio is maintained by scaling both sides as necessary");
ui.checkbox(&mut self.maintain_aspect_ratio, "Maintain aspect ratio");
});
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::new([true, true]).show(ui, |ui| {
let mut image = egui::Image::from_uri(&self.current_uri);
image = image.uv(self.image_options.uv);
image = image.bg_fill(self.image_options.bg_fill);
image = image.tint(self.image_options.tint);
let (angle, origin) = self
.image_options
.rotation
.map_or((0.0, Vec2::splat(0.5)), |(rot, origin)| {
(rot.angle(), origin)
});
image = image.rotate(angle, origin);
match self.fit {
ImageFit::Original { scale } => image = image.fit_to_original_size(scale),
ImageFit::Fraction(fract) => image = image.fit_to_fraction(fract),
ImageFit::Exact(size) => image = image.fit_to_exact_size(size),
}
image = image.maintain_aspect_ratio(self.maintain_aspect_ratio);
image = image.max_size(self.max_size);
ui.add_sized(ui.available_size(), image);
});
});
}
}

View File

@@ -9,6 +9,12 @@ mod fractal_clock;
#[cfg(feature = "http")]
mod http_app;
#[cfg(feature = "image_viewer")]
mod image_viewer;
#[cfg(feature = "image_viewer")]
pub use image_viewer::ImageViewer;
#[cfg(all(feature = "glow", not(feature = "wgpu")))]
pub use custom3d_glow::Custom3d;

View File

@@ -82,6 +82,8 @@ enum Anchor {
EasyMarkEditor,
#[cfg(feature = "http")]
Http,
#[cfg(feature = "image_viewer")]
ImageViewer,
Clock,
#[cfg(any(feature = "glow", feature = "wgpu"))]
Custom3d,
@@ -142,6 +144,8 @@ pub struct State {
easy_mark_editor: EasyMarkApp,
#[cfg(feature = "http")]
http: crate::apps::HttpApp,
#[cfg(feature = "image_viewer")]
image_viewer: crate::apps::ImageViewer,
clock: FractalClockApp,
color_test: ColorTestApp,
@@ -161,6 +165,8 @@ pub struct WrapApp {
impl WrapApp {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
egui_extras::install_image_loaders(&_cc.egui_ctx);
#[allow(unused_mut)]
let mut slf = Self {
state: State::default(),
@@ -204,6 +210,12 @@ impl WrapApp {
Anchor::Clock,
&mut self.state.clock as &mut dyn eframe::App,
),
#[cfg(feature = "image_viewer")]
(
"🖼 Image Viewer",
Anchor::ImageViewer,
&mut self.state.image_viewer as &mut dyn eframe::App,
),
];
#[cfg(any(feature = "glow", feature = "wgpu"))]

View File

@@ -28,16 +28,13 @@ chrono = ["egui_extras/datepicker", "dep:chrono"]
serde = ["egui/serde", "egui_plot/serde", "dep:serde"]
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
syntax_highlighting = ["syntect"]
syntect = ["egui_extras/syntect"]
[dependencies]
egui = { version = "0.22.0", path = "../egui", default-features = false }
egui_extras = { version = "0.22.0", path = "../egui_extras", features = [
"log",
] }
egui_extras = { version = "0.22.0", path = "../egui_extras" }
egui_plot = { version = "0.22.0", path = "../egui_plot" }
enum-map = { version = "2", features = ["serde"] }
log = { version = "0.4", features = ["std"] }
unicode_names2 = { version = "0.6.0", default-features = false }
@@ -46,9 +43,6 @@ chrono = { version = "0.4", optional = true, features = ["js-sys", "wasmbind"] }
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
syntect = { version = "5", optional = true, default-features = false, features = [
"default-fancy",
] }
[dev-dependencies]

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -87,8 +87,12 @@ impl ColorTest {
let tex = self.tex_mngr.get(ui.ctx(), &g);
let texel_offset = 0.5 / (g.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
ui.add(
Image::from_texture((tex.id(), GRADIENT_SIZE))
.tint(vertex_color)
.uv(uv),
)
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
ui.label("GPU result");
});
});
@@ -225,11 +229,15 @@ impl ColorTest {
let tex = self.tex_mngr.get(ui.ctx(), gradient);
let texel_offset = 0.5 / (gradient.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
.on_hover_text(format!(
"A texture that is {} texels wide",
gradient.0.len()
));
ui.add(
Image::from_texture((tex.id(), GRADIENT_SIZE))
.bg_fill(bg_fill)
.uv(uv),
)
.on_hover_text(format!(
"A texture that is {} texels wide",
gradient.0.len()
));
ui.label(label);
});
}

View File

@@ -45,7 +45,6 @@ impl super::View for About {
}
fn about_immediate_mode(ui: &mut egui::Ui) {
use crate::syntax_highlighting::code_view_ui;
ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
ui.horizontal_wrapped(|ui| {
@@ -56,7 +55,7 @@ fn about_immediate_mode(ui: &mut egui::Ui) {
});
ui.add_space(8.0);
code_view_ui(
crate::rust_view_ui(
ui,
r#"
if ui.button("Save").clicked() {

View File

@@ -67,7 +67,7 @@ impl super::View for CodeEditor {
});
}
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
ui.collapsing("Theme", |ui| {
ui.group(|ui| {
theme.ui(ui);
@@ -77,7 +77,7 @@ impl super::View for CodeEditor {
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
let mut layout_job =
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
egui_extras::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job))
};

View File

@@ -81,13 +81,11 @@ impl super::Demo for CodeExample {
impl super::View for CodeExample {
fn ui(&mut self, ui: &mut egui::Ui) {
use crate::syntax_highlighting::code_view_ui;
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file!());
});
code_view_ui(
crate::rust_view_ui(
ui,
r"
pub struct CodeExample {
@@ -117,15 +115,15 @@ impl CodeExample {
});
});
code_view_ui(ui, " }\n}");
crate::rust_view_ui(ui, " }\n}");
ui.separator();
code_view_ui(ui, &format!("{self:#?}"));
crate::rust_view_ui(ui, &format!("{self:#?}"));
ui.separator();
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
ui.collapsing("Theme", |ui| {
theme.ui(ui);
theme.store_in_memory(ui.ctx());
@@ -135,7 +133,7 @@ impl CodeExample {
fn show_code(ui: &mut egui::Ui, code: &str) {
let code = remove_leading_indentation(code.trim_start_matches('\n'));
crate::syntax_highlighting::code_view_ui(ui, &code);
crate::rust_view_ui(ui, &code);
}
fn remove_leading_indentation(code: &str) -> String {

View File

@@ -21,9 +21,6 @@ pub struct WidgetGallery {
#[cfg(feature = "chrono")]
#[cfg_attr(feature = "serde", serde(skip))]
date: Option<chrono::NaiveDate>,
#[cfg_attr(feature = "serde", serde(skip))]
texture: Option<egui::TextureHandle>,
}
impl Default for WidgetGallery {
@@ -39,7 +36,6 @@ impl Default for WidgetGallery {
animate_progress_bar: false,
#[cfg(feature = "chrono")]
date: None,
texture: None,
}
}
}
@@ -111,14 +107,8 @@ impl WidgetGallery {
animate_progress_bar,
#[cfg(feature = "chrono")]
date,
texture,
} = self;
let texture: &egui::TextureHandle = texture.get_or_insert_with(|| {
ui.ctx()
.load_texture("example", egui::ColorImage::example(), Default::default())
});
ui.add(doc_link_label("Label", "label,heading"));
ui.label("Welcome to the widget gallery!");
ui.end_row();
@@ -206,14 +196,19 @@ impl WidgetGallery {
ui.color_edit_button_srgba(color);
ui.end_row();
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
ui.add(doc_link_label("Image", "Image"));
ui.image(texture, img_size);
let egui_icon = egui::include_image!("../../assets/icon.png");
ui.add(egui::Image::new(egui_icon.clone()));
ui.end_row();
ui.add(doc_link_label("ImageButton", "ImageButton"));
if ui.add(egui::ImageButton::new(texture, img_size)).clicked() {
ui.add(doc_link_label(
"Button with image",
"Button::image_and_text",
));
if ui
.add(egui::Button::image_and_text(egui_icon, "Click me!"))
.clicked()
{
*boolean = !*boolean;
}
ui.end_row();

View File

@@ -15,13 +15,19 @@
mod color_test;
mod demo;
pub mod easy_mark;
pub mod syntax_highlighting;
pub use color_test::ColorTest;
pub use demo::DemoWindows;
#[cfg(test)]
use egui::ViewportId;
/// View some Rust code with syntax highlighting and selection.
pub(crate) fn rust_view_ui(ui: &mut egui::Ui, code: &str) {
let language = "rs";
let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language);
}
// ----------------------------------------------------------------------------
/// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file on github.

View File

@@ -24,20 +24,28 @@ all-features = true
[features]
default = []
default = ["dep:mime_guess"]
## Shorthand for enabling `svg`, `image`, and `ehttp`.
all-loaders = ["svg", "image", "http"]
## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`).
all_loaders = ["file", "http", "image", "svg"]
## Enable [`DatePickerButton`] widget.
datepicker = ["chrono"]
## Log warnings using [`log`](https://docs.rs/log) crate.
log = ["dep:log", "egui/log"]
## Add support for loading images from `file://` URIs.
file = ["dep:mime_guess"]
## Add support for loading images via HTTP.
http = ["dep:ehttp"]
## Add support for loading images with the [`image`](https://docs.rs/image) crate.
##
## You also need to ALSO opt-in to the image formats you want to support, like so:
## ```toml
## image = { version = "0.24", features = ["jpeg", "png"] }
## ```
image = ["dep:image"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
@@ -46,9 +54,16 @@ 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 }
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
syntect = ["dep:syntect"]
[dependencies]
egui = { version = "0.22.0", path = "../egui", default-features = false, features = [
"serde",
] }
enum-map = { version = "2", features = ["serde"] }
log = { version = "0.4", features = ["std"] }
serde = { version = "1", features = ["derive"] }
#! ### Optional dependencies
@@ -64,19 +79,17 @@ chrono = { version = "0.4", optional = true, default-features = false, features
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
## Add support for loading images with the [`image`](https://docs.rs/image) crate.
##
## You also need to ALSO opt-in to the image formats you want to support, like so:
## ```toml
## image = { version = "0.24", features = ["jpeg", "png"] }
## ```
image = { version = "0.24", optional = true, default-features = false }
# feature "log"
log = { version = "0.4", optional = true, features = ["std"] }
# file feature
mime_guess = { version = "2.0.4", optional = true, default-features = false }
puffin = { version = "0.16", optional = true }
syntect = { version = "5", optional = true, default-features = false, features = [
"default-fancy",
] }
# svg feature
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

View File

@@ -1,3 +1,5 @@
#![allow(deprecated)]
use egui::{mutex::Mutex, TextureFilter, TextureOptions};
#[cfg(feature = "svg")]
@@ -8,6 +10,9 @@ pub use usvg::FitTo;
/// Load once, and save somewhere in your app state.
///
/// Use the `svg` and `image` features to enable more constructors.
///
/// ⚠ This type is deprecated: Consider using [`egui::Image`] instead.
#[deprecated = "consider using `egui::Image` instead"]
pub struct RetainedImage {
debug_name: String,
@@ -151,7 +156,7 @@ impl RetainedImage {
&self.debug_name
}
/// The texture if for this image.
/// The texture id for this image.
pub fn texture_id(&self, ctx: &egui::Context) -> egui::TextureId {
self.texture
.lock()
@@ -186,7 +191,7 @@ impl RetainedImage {
// We need to convert the SVG to a texture to display it:
// Future improvement: tell backend to do mip-mapping of the image to
// make it look smoother when downsized.
ui.image(self.texture_id(ui.ctx()), desired_size)
ui.image((self.texture_id(ui.ctx()), desired_size))
}
}

View File

@@ -13,6 +13,9 @@
#[cfg(feature = "chrono")]
mod datepicker;
pub mod syntax_highlighting;
#[doc(hidden)]
pub mod image;
mod layout;
pub mod loaders;
@@ -23,12 +26,16 @@ mod table;
#[cfg(feature = "chrono")]
pub use crate::datepicker::DatePickerButton;
#[doc(hidden)]
#[allow(deprecated)]
pub use crate::image::RetainedImage;
pub(crate) use crate::layout::StripLayout;
pub use crate::sizing::Size;
pub use crate::strip::*;
pub use crate::table::*;
pub use loaders::install_image_loaders;
// ---------------------------------------------------------------------------
mod profiling_scopes {
@@ -38,8 +45,8 @@ mod profiling_scopes {
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
@@ -48,8 +55,8 @@ mod profiling_scopes {
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
@@ -61,56 +68,15 @@ pub(crate) use profiling_scopes::*;
// ---------------------------------------------------------------------------
/// Log an error with either `log` or `eprintln`
macro_rules! log_err {
($fmt: literal, $($arg: tt)*) => {{
#[cfg(feature = "log")]
log::error!($fmt, $($arg)*);
#[cfg(not(feature = "log"))]
eprintln!(
concat!("egui_extras: ", $fmt), $($arg)*
);
}};
}
pub(crate) use log_err;
/// Panic in debug builds, log otherwise.
macro_rules! log_or_panic {
($fmt: literal) => {$crate::log_or_panic!($fmt,)};
($fmt: literal, $($arg: tt)*) => {{
if cfg!(debug_assertions) {
panic!($fmt, $($arg)*);
} else {
$crate::log_err!($fmt, $($arg)*);
log::error!($fmt, $($arg)*);
}
}};
}
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;

View File

@@ -1,46 +1,96 @@
// 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
/// Installs a set of image loaders.
///
/// ⚠ 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.
/// Calling this enables the use of [`egui::Image`] and [`egui::Ui::image`].
///
/// ⚠ The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image)
/// ⚠ This will do nothing and you won't see any images unless you also enable some feature flags on `egui_extras`:
///
/// - `file` feature: `file://` loader on non-Wasm targets
/// - `http` feature: `http(s)://` loader
/// - `image` feature: Loader of png, jpeg etc using the [`image`] crate
/// - `svg` feature: `.svg` loader
///
/// Calling this multiple times on the same [`egui::Context`] is safe.
/// It will never install duplicate loaders.
///
/// - If you just want to be able to load `file://` and `http://` URIs, 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
/// egui_extras = { version = "*", features = ["all_loaders"] }
/// image = { version = "0.24", features = ["jpeg", "png"] }
/// ```
///
/// ⚠ You have to configure both the supported loaders in `egui_extras` _and_ the supported image formats
/// in `image` to get any output!
///
/// ## Loader-specific information
///
/// ⚠ The exact way bytes, images, and textures are loaded is subject to change,
/// but the supported protocols and file extensions are not.
///
/// The `file` loader is a [`BytesLoader`][`egui::load::BytesLoader`].
/// It will attempt to load `file://` URIs, and infer the content type from the extension.
/// The path will be passed to [`std::fs::read`] after trimming the `file://` prefix,
/// and is resolved the same way as with `std::fs::read(path)`:
/// - Relative paths are relative to the current working directory
/// - Absolute paths are left as is.
///
/// The `http` loader is a [`BytesLoader`][`egui::load::BytesLoader`].
/// It will attempt to load `http://` and `https://` URIs, and infer the content type from the `Content-Type` header.
///
/// The `image` loader is an [`ImageLoader`][`egui::load::ImageLoader`].
/// It will attempt to load any URI with any extension other than `svg`.
/// It will also try to load any URI without an extension.
/// The content type specified by [`BytesPoll::Ready::mime`][`egui::load::BytesPoll::Ready::mime`] always takes precedence.
/// This means that even if the URI has a `png` extension, and the `png` image format is enabled, if the content type is
/// not one of the supported and enabled image formats, the loader will return [`LoadError::NotSupported`][`egui::load::LoadError::NotSupported`],
/// allowing a different loader to attempt to load the image.
///
/// The `svg` loader is an [`ImageLoader`][`egui::load::ImageLoader`].
/// It will attempt to load any URI with an `svg` extension. It will _not_ attempt to load a URI without an extension.
/// The content type specified by [`BytesPoll::Ready::mime`][`egui::load::BytesPoll::Ready::mime`] always takes precedence,
/// and must include `svg` for it to be considered supported. For example, `image/svg+xml` would be loaded by the `svg` loader.
///
/// 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()));
pub fn install_image_loaders(ctx: &egui::Context) {
#[cfg(all(not(target_arch = "wasm32"), feature = "file"))]
if !ctx.is_loader_installed(self::file_loader::FileLoader::ID) {
ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default()));
log::trace!("installed FileLoader");
}
#[cfg(feature = "http")]
ctx.add_bytes_loader(std::sync::Arc::new(
self::ehttp_loader::EhttpLoader::default(),
));
if !ctx.is_loader_installed(self::ehttp_loader::EhttpLoader::ID) {
ctx.add_bytes_loader(std::sync::Arc::new(
self::ehttp_loader::EhttpLoader::default(),
));
log::trace!("installed EhttpLoader");
}
#[cfg(feature = "image")]
ctx.add_image_loader(std::sync::Arc::new(
self::image_loader::ImageCrateLoader::default(),
));
if !ctx.is_loader_installed(self::image_loader::ImageCrateLoader::ID) {
ctx.add_image_loader(std::sync::Arc::new(
self::image_loader::ImageCrateLoader::default(),
));
log::trace!("installed ImageCrateLoader");
}
#[cfg(feature = "svg")]
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
if !ctx.is_loader_installed(self::svg_loader::SvgLoader::ID) {
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
log::trace!("installed SvgLoader");
}
#[cfg(all(
target_arch = "wasm32",
any(target_arch = "wasm32", not(feature = "file")),
not(feature = "http"),
not(feature = "image"),
not(feature = "svg")
))]
crate::log_warn!("`loaders::install` was called, but no loaders are enabled");
log::warn!("`install_image_loaders` was called, but no loaders are enabled");
let _ = ctx;
}

View File

@@ -5,52 +5,60 @@ use egui::{
};
use std::{sync::Arc, task::Poll};
type Entry = Poll<Result<Arc<[u8]>, String>>;
#[derive(Clone)]
struct File {
bytes: Arc<[u8]>,
mime: Option<String>,
}
impl File {
fn from_response(uri: &str, response: ehttp::Response) -> Result<Self, String> {
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 mime = response.content_type().map(|v| v.to_owned());
let bytes = response.bytes.into();
Ok(File { bytes, mime })
}
}
type Entry = Poll<Result<File, String>>;
#[derive(Default)]
pub struct EhttpLoader {
cache: Arc<Mutex<HashMap<String, Entry>>>,
}
impl EhttpLoader {
pub const ID: &str = egui::generate_loader_id!(EhttpLoader);
}
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 id(&self) -> &str {
Self::ID
}
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
if !starts_with_one_of(uri, PROTOCOLS) {
return Err(LoadError::NotSupported);
@@ -59,15 +67,16 @@ impl BytesLoader for EhttpLoader {
let mut cache = self.cache.lock();
if let Some(entry) = cache.get(uri).cloned() {
match entry {
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
size: None,
bytes: Bytes::Shared(bytes),
bytes: Bytes::Shared(file.bytes),
mime: file.mime,
}),
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
Poll::Ready(Err(err)) => Err(LoadError::Loading(err)),
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
}
} else {
crate::log_trace!("started loading {uri:?}");
log::trace!("started loading {uri:?}");
let uri = uri.to_owned();
cache.insert(uri.clone(), Poll::Pending);
@@ -77,8 +86,15 @@ impl BytesLoader for EhttpLoader {
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 result = match response {
Ok(response) => File::from_response(&uri, response),
Err(err) => {
// Log details; return summary
log::error!("Failed to load {uri:?}: {err}");
Err(format!("Failed to load {uri:?}"))
}
};
log::trace!("finished loading {uri:?}");
let prev = cache.lock().insert(uri, Poll::Ready(result));
assert!(matches!(prev, Some(Poll::Pending)));
ctx.request_repaint();
@@ -93,12 +109,18 @@ impl BytesLoader for EhttpLoader {
let _ = self.cache.lock().remove(uri);
}
fn forget_all(&self) {
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|entry| match entry {
Poll::Ready(Ok(bytes)) => bytes.len(),
Poll::Ready(Ok(file)) => {
file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len())
}
Poll::Ready(Err(err)) => err.len(),
_ => 0,
})

View File

@@ -5,7 +5,13 @@ use egui::{
};
use std::{sync::Arc, task::Poll, thread};
type Entry = Poll<Result<Arc<[u8]>, String>>;
#[derive(Clone)]
struct File {
bytes: Arc<[u8]>,
mime: Option<String>,
}
type Entry = Poll<Result<File, String>>;
#[derive(Default)]
pub struct FileLoader {
@@ -13,9 +19,17 @@ pub struct FileLoader {
cache: Arc<Mutex<HashMap<String, Entry>>>,
}
impl FileLoader {
pub const ID: &str = egui::generate_loader_id!(FileLoader);
}
const PROTOCOL: &str = "file://";
impl BytesLoader for FileLoader {
fn id(&self) -> &str {
Self::ID
}
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
// File loader only supports the `file` protocol.
let Some(path) = uri.strip_prefix(PROTOCOL) else {
@@ -26,15 +40,16 @@ impl BytesLoader for FileLoader {
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 {
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
size: None,
bytes: Bytes::Shared(bytes),
bytes: Bytes::Shared(file.bytes),
mime: file.mime,
}),
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
Poll::Ready(Err(err)) => Err(LoadError::Loading(err)),
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
}
} else {
crate::log_trace!("started loading {uri:?}");
log::trace!("started loading {uri:?}");
// We need to load the file at `path`.
// Set the file to `pending` until we finish loading it.
@@ -48,16 +63,29 @@ impl BytesLoader for FileLoader {
.spawn({
let ctx = ctx.clone();
let cache = self.cache.clone();
let uri = uri.to_owned();
let _uri = uri.to_owned();
move || {
let result = match std::fs::read(&path) {
Ok(bytes) => Ok(bytes.into()),
Ok(bytes) => {
#[cfg(feature = "mime_guess")]
let mime = mime_guess::from_path(&path)
.first_raw()
.map(|v| v.to_owned());
#[cfg(not(feature = "mime_guess"))]
let mime = None;
Ok(File {
bytes: bytes.into(),
mime,
})
}
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:?}");
log::trace!("finished loading {_uri:?}");
}
})
.expect("failed to spawn thread");
@@ -70,12 +98,18 @@ impl BytesLoader for FileLoader {
let _ = self.cache.lock().remove(uri);
}
fn forget_all(&self) {
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|entry| match entry {
Poll::Ready(Ok(bytes)) => bytes.len(),
Poll::Ready(Ok(file)) => {
file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len())
}
Poll::Ready(Err(err)) => err.len(),
_ => 0,
})

View File

@@ -13,18 +13,36 @@ pub struct ImageCrateLoader {
cache: Mutex<HashMap<String, Entry>>,
}
fn is_supported(uri: &str) -> bool {
impl ImageCrateLoader {
pub const ID: &str = egui::generate_loader_id!(ImageCrateLoader);
}
fn is_supported_uri(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
return true;
};
ext != "svg"
}
fn is_unsupported_mime(mime: &str) -> bool {
mime.contains("svg")
}
impl ImageLoader for ImageCrateLoader {
fn id(&self) -> &str {
Self::ID
}
fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult {
if !is_supported(uri) {
// three stages of guessing if we support loading the image:
// 1. URI extension
// 2. Mime from `BytesPoll::Ready`
// 3. image::guess_format
// (1)
if !is_supported_uri(uri) {
return Err(LoadError::NotSupported);
}
@@ -32,18 +50,25 @@ impl ImageLoader for ImageCrateLoader {
if let Some(entry) = cache.get(uri).cloned() {
match entry {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)),
Err(err) => Err(LoadError::Loading(err)),
}
} else {
match ctx.try_load_bytes(uri) {
Ok(BytesPoll::Ready { bytes, .. }) => {
crate::log_trace!("started loading {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);
}
log::trace!("started loading {uri:?}");
let result = crate::image::load_image_bytes(&bytes).map(Arc::new);
crate::log_trace!("finished loading {uri:?}");
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)),
Err(err) => Err(LoadError::Loading(err)),
}
}
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
@@ -56,6 +81,10 @@ impl ImageLoader for ImageCrateLoader {
let _ = self.cache.lock().remove(uri);
}
fn forget_all(&self) {
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache
.lock()
@@ -74,11 +103,11 @@ mod tests {
#[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"));
assert!(is_supported_uri("https://test.png"));
assert!(is_supported_uri("test.jpeg"));
assert!(is_supported_uri("http://test.gif"));
assert!(is_supported_uri("test.webp"));
assert!(is_supported_uri("file://test"));
assert!(!is_supported_uri("test.svg"));
}
}

View File

@@ -13,13 +13,23 @@ pub struct SvgLoader {
cache: Mutex<HashMap<(String, SizeHint), Entry>>,
}
impl SvgLoader {
pub const ID: &str = egui::generate_loader_id!(SvgLoader);
}
fn is_supported(uri: &str) -> bool {
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else { return false };
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else {
return false;
};
ext == "svg"
}
impl ImageLoader for SvgLoader {
fn id(&self) -> &str {
Self::ID
}
fn load(&self, ctx: &egui::Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult {
if !is_supported(uri) {
return Err(LoadError::NotSupported);
@@ -32,25 +42,25 @@ impl ImageLoader for SvgLoader {
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)),
Err(err) => Err(LoadError::Loading(err)),
}
} else {
match ctx.try_load_bytes(&uri) {
Ok(BytesPoll::Ready { bytes, .. }) => {
crate::log_trace!("started loading {uri:?}");
log::trace!("started loading {uri:?}");
let fit_to = match size_hint {
SizeHint::Original => usvg::FitTo::Original,
SizeHint::Scale(factor) => usvg::FitTo::Zoom(factor.into_inner()),
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:?}");
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)),
Err(err) => Err(LoadError::Loading(err)),
}
}
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
@@ -63,6 +73,10 @@ impl ImageLoader for SvgLoader {
self.cache.lock().retain(|(u, _), _| u != uri);
}
fn forget_all(&self) {
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache
.lock()

View File

@@ -153,7 +153,7 @@ impl From<Vec<Size>> for Sizing {
#[test]
fn test_sizing() {
let sizing: Sizing = vec![].into();
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![]);
assert_eq!(sizing.to_lengths(50.0, 0.0), Vec::<f32>::new());
let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into();
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]);

View File

@@ -1,12 +1,19 @@
//! Syntax highlighting for code.
//!
//! Turn on the `syntect` feature for great syntax highlighting of any language.
//! Otherwise, a very simple fallback will be used, that works okish for C, C++, Rust, and Python.
use egui::text::LayoutJob;
/// View some code with syntax highlighting and selection.
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
let language = "rs";
let theme = CodeTheme::from_memory(ui.ctx());
pub fn code_view_ui(
ui: &mut egui::Ui,
theme: &CodeTheme,
mut code: &str,
language: &str,
) -> egui::Response {
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
let layout_job = highlight(ui.ctx(), &theme, string, language);
let layout_job = highlight(ui.ctx(), theme, string, language);
// layout_job.wrap.max_width = wrap_width; // no wrapping
ui.fonts(|f| f.layout_job(layout_job))
};
@@ -18,10 +25,12 @@ pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
.desired_rows(1)
.lock_focus(true)
.layouter(&mut layouter),
);
)
}
/// Memoized Code highlighting
/// Add syntax highlighting to a code string.
///
/// The results are memoized, so you can call this every frame without performance penalty.
pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
@@ -41,9 +50,7 @@ pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &
// ----------------------------------------------------------------------------
#[cfg(not(feature = "syntect"))]
#[derive(Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(enum_map::Enum)]
#[derive(Clone, Copy, PartialEq, serde::Deserialize, serde::Serialize, enum_map::Enum)]
enum TokenType {
Comment,
Keyword,
@@ -54,8 +61,7 @@ enum TokenType {
}
#[cfg(feature = "syntect")]
#[derive(Clone, Copy, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
enum SyntectTheme {
Base16EightiesDark,
Base16MochaDark,
@@ -118,9 +124,9 @@ impl SyntectTheme {
}
}
#[derive(Clone, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
/// A selected color theme.
#[derive(Clone, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct CodeTheme {
dark_mode: bool,
@@ -138,6 +144,7 @@ impl Default for CodeTheme {
}
impl CodeTheme {
/// Selects either dark or light theme based on the given style.
pub fn from_style(style: &egui::Style) -> Self {
if style.visuals.dark_mode {
Self::dark()
@@ -146,6 +153,9 @@ impl CodeTheme {
}
}
/// Load code theme from egui memory.
///
/// There is one dark and one light theme stored at any one time.
pub fn from_memory(ctx: &egui::Context) -> Self {
if ctx.style().visuals.dark_mode {
ctx.data_mut(|d| {
@@ -160,6 +170,9 @@ impl CodeTheme {
}
}
/// Store theme to egui memory.
///
/// There is one dark and one light theme stored at any one time.
pub fn store_in_memory(self, ctx: &egui::Context) {
if self.dark_mode {
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self));
@@ -185,6 +198,7 @@ impl CodeTheme {
}
}
/// Show UI for changing the color theme.
pub fn ui(&mut self, ui: &mut egui::Ui) {
egui::widgets::global_dark_light_mode_buttons(ui);
@@ -231,6 +245,7 @@ impl CodeTheme {
}
}
/// Show UI for changing the color theme.
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.horizontal_top(|ui| {
let selected_id = egui::Id::null();
@@ -306,6 +321,7 @@ struct Highlighter {
#[cfg(feature = "syntect")]
impl Default for Highlighter {
fn default() -> Self {
crate::profile_function!();
Self {
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
ts: syntect::highlighting::ThemeSet::load_defaults(),
@@ -313,7 +329,6 @@ impl Default for Highlighter {
}
}
#[cfg(feature = "syntect")]
impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
@@ -332,7 +347,10 @@ impl Highlighter {
})
}
#[cfg(feature = "syntect")]
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
crate::profile_function!();
use syntect::easy::HighlightLines;
use syntect::highlighting::FontStyle;
use syntect::util::LinesWithEndings;
@@ -400,13 +418,24 @@ struct Highlighter {}
#[cfg(not(feature = "syntect"))]
impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob {
fn highlight_impl(
&self,
theme: &CodeTheme,
mut text: &str,
language: &str,
) -> Option<LayoutJob> {
crate::profile_function!();
let language = Language::new(language)?;
// Extremely simple syntax highlighter for when we compile without syntect
let mut job = LayoutJob::default();
while !text.is_empty() {
if text.starts_with("//") {
if language.double_slash_comments && text.starts_with("//")
|| language.hash_comments && text.starts_with('#')
{
let end = text.find('\n').unwrap_or(text.len());
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
text = &text[end..];
@@ -427,7 +456,7 @@ impl Highlighter {
.find(|c: char| !c.is_ascii_alphanumeric())
.map_or_else(|| text.len(), |i| i + 1);
let word = &text[..end];
let tt = if is_keyword(word) {
let tt = if language.is_keyword(word) {
TokenType::Keyword
} else {
TokenType::Literal
@@ -457,50 +486,173 @@ impl Highlighter {
}
}
job
Some(job)
}
}
#[cfg(not(feature = "syntect"))]
fn is_keyword(word: &str) -> bool {
matches!(
word,
"as" | "async"
| "await"
| "break"
| "const"
| "continue"
| "crate"
| "dyn"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "Self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
)
struct Language {
/// `// comment`
double_slash_comments: bool,
/// `# comment`
hash_comments: bool,
keywords: std::collections::BTreeSet<&'static str>,
}
#[cfg(not(feature = "syntect"))]
impl Language {
fn new(language: &str) -> Option<Self> {
match language.to_lowercase().as_str() {
"c" | "h" | "hpp" | "cpp" | "c++" => Some(Self::cpp()),
"py" | "python" => Some(Self::python()),
"rs" | "rust" => Some(Self::rust()),
_ => {
None // unsupported language
}
}
}
fn is_keyword(&self, word: &str) -> bool {
self.keywords.contains(word)
}
fn cpp() -> Self {
Self {
double_slash_comments: true,
hash_comments: false,
keywords: [
"alignas",
"alignof",
"and_eq",
"and",
"asm",
"atomic_cancel",
"atomic_commit",
"atomic_noexcept",
"auto",
"bitand",
"bitor",
"bool",
"break",
"case",
"catch",
"char",
"char16_t",
"char32_t",
"char8_t",
"class",
"co_await",
"co_return",
"co_yield",
"compl",
"concept",
"const_cast",
"const",
"consteval",
"constexpr",
"constinit",
"continue",
"decltype",
"default",
"delete",
"do",
"double",
"dynamic_cast",
"else",
"enum",
"explicit",
"export",
"extern",
"false",
"float",
"for",
"friend",
"goto",
"if",
"inline",
"int",
"long",
"mutable",
"namespace",
"new",
"noexcept",
"not_eq",
"not",
"nullptr",
"operator",
"or_eq",
"or",
"private",
"protected",
"public",
"reflexpr",
"register",
"reinterpret_cast",
"requires",
"return",
"short",
"signed",
"sizeof",
"static_assert",
"static_cast",
"static",
"struct",
"switch",
"synchronized",
"template",
"this",
"thread_local",
"throw",
"true",
"try",
"typedef",
"typeid",
"typename",
"union",
"unsigned",
"using",
"virtual",
"void",
"volatile",
"wchar_t",
"while",
"xor_eq",
"xor",
]
.into_iter()
.collect(),
}
}
fn python() -> Self {
Self {
double_slash_comments: false,
hash_comments: true,
keywords: [
"and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else",
"except", "False", "finally", "for", "from", "global", "if", "import", "in", "is",
"lambda", "None", "nonlocal", "not", "or", "pass", "raise", "return", "True",
"try", "while", "with", "yield",
]
.into_iter()
.collect(),
}
}
fn rust() -> Self {
Self {
double_slash_comments: true,
hash_comments: false,
keywords: [
"as", "async", "await", "break", "const", "continue", "crate", "dyn", "else",
"enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match",
"mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
"super", "trait", "true", "type", "unsafe", "use", "where", "while",
]
.into_iter()
.collect(),
}
}
}

View File

@@ -648,7 +648,9 @@ impl<'a> Table<'a> {
// If the last column is 'remainder', then let it fill the remainder!
let eps = 0.1; // just to avoid some rounding errors.
*column_width = available_width - eps;
*column_width = column_width.at_least(max_used_widths[i]);
if !column.clip {
*column_width = column_width.at_least(max_used_widths[i]);
}
*column_width = width_range.clamp(*column_width);
break;
}

View File

@@ -30,8 +30,7 @@ fn main() {
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
if ui
.add(egui::Button::image_and_text(
texture_id,
button_image_size,
(texture_id, button_image_size),
"Quit",
))
.clicked()

View File

@@ -112,22 +112,30 @@ pub fn check_for_gl_error_impl(gl: &glow::Context, file: &str, line: u32, contex
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))]
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
mod profiling_scopes {
#![allow(unused_macros)]
#![allow(unused_imports)]
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))]
puffin::profile_scope!($($arg)*);
};
/// Profiling macro for feature "puffin"
macro_rules! profile_function {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_function!($($arg)*);
};
}
pub(crate) use profile_function;
/// Profiling macro for feature "puffin"
macro_rules! profile_scope {
($($arg: tt)*) => {
#[cfg(feature = "puffin")]
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
puffin::profile_scope!($($arg)*);
};
}
pub(crate) use profile_scope;
}
pub(crate) use profile_scope;
#[allow(unused_imports)]
pub(crate) use profiling_scopes::*;

View File

@@ -370,25 +370,6 @@ impl Painter {
Primitive::Callback(callback) => {
if callback.rect.is_positive() {
crate::profile_scope!("callback");
// Transform callback rect to physical pixels:
let rect_min_x = pixels_per_point * callback.rect.min.x;
let rect_min_y = pixels_per_point * callback.rect.min.y;
let rect_max_x = pixels_per_point * callback.rect.max.x;
let rect_max_y = pixels_per_point * callback.rect.max.y;
let rect_min_x = rect_min_x.round() as i32;
let rect_min_y = rect_min_y.round() as i32;
let rect_max_x = rect_max_x.round() as i32;
let rect_max_y = rect_max_y.round() as i32;
unsafe {
self.gl.viewport(
rect_min_x,
size_in_pixels.1 as i32 - rect_max_y,
rect_max_x - rect_min_x,
rect_max_y - rect_min_y,
);
}
let info = egui::PaintCallbackInfo {
viewport: callback.rect,
@@ -397,6 +378,16 @@ impl Painter {
screen_size_px,
};
let viewport_px = info.viewport_in_pixels();
unsafe {
self.gl.viewport(
viewport_px.left_px.round() as _,
viewport_px.from_bottom_px.round() as _,
viewport_px.width_px.round() as _,
viewport_px.height_px.round() as _,
);
}
if let Some(callback) = callback.callback.downcast_ref::<CallbackFn>() {
(callback.f)(info, self);
} else {
@@ -494,10 +485,13 @@ impl Painter {
"Mismatch between texture size and texel count"
);
let data: Vec<u8> = image
.srgba_pixels(None)
.flat_map(|a| a.to_array())
.collect();
let data: Vec<u8> = {
crate::profile_scope!("font -> sRGBA");
image
.srgba_pixels(None)
.flat_map(|a| a.to_array())
.collect()
};
self.upload_texture_srgb(delta.pos, image.size, delta.options, &data);
}
@@ -511,6 +505,7 @@ impl Painter {
options: egui::TextureOptions,
data: &[u8],
) {
crate::profile_function!();
assert_eq!(data.len(), w * h * 4);
assert!(
w <= self.max_texture_side && h <= self.max_texture_side,
@@ -561,6 +556,7 @@ impl Painter {
let level = 0;
if let Some([x, y]) = pos {
crate::profile_scope!("gl.tex_sub_image_2d");
self.gl.tex_sub_image_2d(
glow::TEXTURE_2D,
level,
@@ -575,6 +571,7 @@ impl Painter {
check_for_gl_error!(&self.gl, "tex_sub_image_2d");
} else {
let border = 0;
crate::profile_scope!("gl.tex_image_2d");
self.gl.tex_image_2d(
glow::TEXTURE_2D,
level,

View File

@@ -1234,12 +1234,19 @@ impl PlotItem for PlotImage {
Rect::from_two_pos(left_top_screen, right_bottom_screen)
};
let screen_rotation = -*rotation as f32;
Image::new(*texture_id, image_screen_rect.size())
.bg_fill(*bg_fill)
.tint(*tint)
.uv(*uv)
.rotate(screen_rotation, Vec2::splat(0.5))
.paint_at(ui, image_screen_rect);
egui::paint_texture_at(
ui.painter(),
image_screen_rect,
&ImageOptions {
uv: *uv,
bg_fill: *bg_fill,
tint: *tint,
rotation: Some((Rot2::from_angle(screen_rotation), Vec2::splat(0.5))),
rounding: Rounding::ZERO,
},
&(*texture_id, image_screen_rect.size()).into(),
);
if *highlight {
let center = image_screen_rect.center();
let rotation = Rot2::from_angle(screen_rotation);

View File

@@ -280,6 +280,7 @@ impl FontImage {
/// `gamma` should normally be set to `None`.
///
/// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`.
#[inline]
pub fn srgba_pixels(
&'_ self,
gamma: Option<f32>,
@@ -338,6 +339,7 @@ impl From<FontImage> for ImageData {
}
}
#[inline]
fn fast_round(r: f32) -> u8 {
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
}

View File

@@ -787,6 +787,8 @@ pub struct PaintCallbackInfo {
/// Rect is the [-1, +1] of the Normalized Device Coordinates.
///
/// Note than only a portion of this may be visible due to [`Self::clip_rect`].
///
/// This comes from [`PaintCallback::rect`].
pub viewport: Rect,
/// Clip rectangle in points.
@@ -819,7 +821,7 @@ pub struct ViewportInPixels {
}
impl PaintCallbackInfo {
fn points_to_pixels(&self, rect: &Rect) -> ViewportInPixels {
fn pixels_from_points(&self, rect: &Rect) -> ViewportInPixels {
ViewportInPixels {
left_px: rect.min.x * self.pixels_per_point,
top_px: rect.min.y * self.pixels_per_point,
@@ -831,12 +833,12 @@ impl PaintCallbackInfo {
/// The viewport rectangle. This is what you would use in e.g. `glViewport`.
pub fn viewport_in_pixels(&self) -> ViewportInPixels {
self.points_to_pixels(&self.viewport)
self.pixels_from_points(&self.viewport)
}
/// The "scissor" or "clip" rectangle. This is what you would use in e.g. `glScissor`.
pub fn clip_rect_in_pixels(&self) -> ViewportInPixels {
self.points_to_pixels(&self.clip_rect)
self.pixels_from_points(&self.clip_rect)
}
}
@@ -846,6 +848,8 @@ impl PaintCallbackInfo {
#[derive(Clone)]
pub struct PaintCallback {
/// Where to paint.
///
/// This will become [`PaintCallbackInfo::viewport`].
pub rect: Rect,
/// Paint something custom (e.g. 3D stuff).

View File

@@ -1063,7 +1063,7 @@ fn mul_color(color: Color32, factor: f32) -> Color32 {
///
/// For performance reasons it is smart to reuse the same [`Tessellator`].
///
/// Se also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`].
/// See also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`].
pub struct Tessellator {
pixels_per_point: f32,
options: TessellationOptions,

View File

@@ -43,12 +43,6 @@ pub struct GlyphInfo {
/// Unit: points.
pub advance_width: f32,
/// `ascent` value from the font metrics.
/// this is the distance from the top to the baseline.
///
/// Unit: points.
pub ascent: f32,
/// Texture coordinates.
pub uv_rect: UvRect,
}
@@ -59,7 +53,6 @@ impl Default for GlyphInfo {
Self {
id: ab_glyph::GlyphId(0),
advance_width: 0.0,
ascent: 0.0,
uv_rect: Default::default(),
}
}
@@ -188,7 +181,7 @@ impl FontImpl {
if let Some(space) = self.glyph_info(' ') {
let glyph_info = GlyphInfo {
advance_width: crate::text::TAB_SIZE as f32 * space.advance_width,
..GlyphInfo::default()
..space
};
self.glyph_info_cache.write().insert(c, glyph_info);
return Some(glyph_info);
@@ -205,7 +198,7 @@ impl FontImpl {
let advance_width = f32::min(em / 6.0, space.advance_width * 0.5);
let glyph_info = GlyphInfo {
advance_width,
..GlyphInfo::default()
..space
};
self.glyph_info_cache.write().insert(c, glyph_info);
return Some(glyph_info);
@@ -255,6 +248,14 @@ impl FontImpl {
self.pixels_per_point
}
/// This is the distance from the top to the baseline.
///
/// Unit: points.
#[inline(always)]
pub fn ascent(&self) -> f32 {
self.ascent
}
fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo {
assert!(glyph_id.0 != 0);
use ab_glyph::{Font as _, ScaleFont};
@@ -305,7 +306,6 @@ impl FontImpl {
GlyphInfo {
id: glyph_id,
advance_width: advance_width_in_points,
ascent: self.ascent,
uv_rect,
}
}
@@ -442,7 +442,7 @@ impl Font {
}
#[inline]
pub(crate) fn glyph_info_and_font_impl(&mut self, c: char) -> (Option<&FontImpl>, GlyphInfo) {
pub(crate) fn font_impl_and_glyph_info(&mut self, c: char) -> (Option<&FontImpl>, GlyphInfo) {
if self.fonts.is_empty() {
return (None, self.replacement_glyph.1);
}

View File

@@ -134,7 +134,7 @@ fn layout_section(
paragraph = out_paragraphs.last_mut().unwrap();
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
} else {
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(chr);
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(chr);
if let Some(font_impl) = font_impl {
if let Some(last_glyph_id) = last_glyph_id {
paragraph.cursor_x += font_impl.pair_kerning(last_glyph_id, glyph_info.id);
@@ -146,7 +146,7 @@ fn layout_section(
chr,
pos: pos2(paragraph.cursor_x, f32::NAN),
size: vec2(glyph_info.advance_width, line_height),
ascent: glyph_info.ascent,
ascent: font_impl.map_or(0.0, |font| font.ascent()), // Failure to find the font here would be weird
uv_rect: glyph_info.uv_rect,
section_index,
});
@@ -340,12 +340,12 @@ fn replace_last_glyph_with_overflow_character(
.unwrap_or_else(|| font.row_height());
let prev_glyph_id = prev_glyph.map(|prev_glyph| {
let (_, prev_glyph_info) = font.glyph_info_and_font_impl(prev_glyph.chr);
let (_, prev_glyph_info) = font.font_impl_and_glyph_info(prev_glyph.chr);
prev_glyph_info.id
});
// undo kerning with previous glyph
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr);
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
last_glyph.pos.x -= extra_letter_spacing
+ font_impl
.zip(prev_glyph_id)
@@ -356,10 +356,9 @@ fn replace_last_glyph_with_overflow_character(
// replace the glyph
last_glyph.chr = overflow_character;
let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr);
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
last_glyph.size = vec2(glyph_info.advance_width, line_height);
last_glyph.uv_rect = glyph_info.uv_rect;
last_glyph.ascent = glyph_info.ascent;
// reapply kerning
last_glyph.pos.x += extra_letter_spacing

View File

@@ -86,7 +86,10 @@ impl TextureHandle {
/// width x height
pub fn size(&self) -> [usize; 2] {
self.tex_mngr.read().meta(self.id).unwrap().size
self.tex_mngr
.read()
.meta(self.id)
.map_or([0, 0], |tex| tex.size)
}
/// width x height
@@ -97,7 +100,10 @@ impl TextureHandle {
/// `width x height x bytes_per_pixel`
pub fn byte_size(&self) -> usize {
self.tex_mngr.read().meta(self.id).unwrap().bytes_used()
self.tex_mngr
.read()
.meta(self.id)
.map_or(0, |tex| tex.bytes_used())
}
/// width / height
@@ -108,7 +114,10 @@ impl TextureHandle {
/// Debug-name.
pub fn name(&self) -> String {
self.tex_mngr.read().meta(self.id).unwrap().name.clone()
self.tex_mngr
.read()
.meta(self.id)
.map_or_else(|| "<none>".to_owned(), |tex| tex.name.clone())
}
}

View File

@@ -8,8 +8,16 @@ use std::hash::{Hash, Hasher};
/// Possible types for `T` are `f32` and `f64`.
///
/// See also [`FloatOrd`].
#[derive(Clone, Copy)]
pub struct OrderedFloat<T>(T);
impl<T: Float + Copy> OrderedFloat<T> {
#[inline]
pub fn into_inner(self) -> T {
self.0
}
}
impl<T: Float> Eq for OrderedFloat<T> {}
impl<T: Float> PartialEq<Self> for OrderedFloat<T> {

View File

@@ -1,21 +0,0 @@
[package]
name = "download_image"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.70"
publish = false
[dependencies]
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 = [
"http",
"image",
"log",
] }
env_logger = "0.10"
image = { version = "0.24", default-features = false, features = ["jpeg"] }

View File

@@ -1,7 +0,0 @@
Example how to download and show an image with eframe/egui.
```sh
cargo run -p download_image
```
![](screenshot.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

View File

@@ -1,41 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions::default();
eframe::run_native(
"Download and show an image with eframe/egui",
options,
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;
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
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",
))
});
});
}
}

View File

@@ -1,7 +1,7 @@
[package]
name = "retained_image"
name = "images"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
authors = ["Jan Procházka <github.com/jprochazk>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.70"
@@ -10,8 +10,11 @@ publish = false
[dependencies]
eframe = { path = "../../crates/eframe", features = [
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
egui_extras = { path = "../../crates/egui_extras", features = ["image", "log"] }
egui_extras = { path = "../../crates/egui_extras", features = ["all_loaders"] }
env_logger = "0.10"
image = { version = "0.24", default-features = false, features = ["png"] }
image = { version = "0.24", default-features = false, features = [
"jpeg",
"png",
] }

View File

@@ -0,0 +1,7 @@
Example showing how to show images with eframe/egui.
```sh
cargo run -p images
```
![](screenshot.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,38 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(600.0, 800.0)),
..Default::default()
};
eframe::run_native(
"Image Viewer",
options,
Box::new(|cc| {
// The following call is needed to load images when using `ui.image` and `egui::Image`:
egui_extras::install_image_loaders(&cc.egui_ctx);
Box::<MyApp>::default()
}),
)
}
#[derive(Default)]
struct MyApp {}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::new([true, true]).show(ui, |ui| {
ui.image(egui::include_image!("ferris.svg"));
ui.add(
egui::Image::new("https://picsum.photos/seed/1.759706314/1024")
.rounding(egui::Rounding::same(10.0)),
);
});
});
}
}

View File

@@ -56,6 +56,12 @@ fn start_puffin_server() {
Ok(puffin_server) => {
eprintln!("Run: cargo install puffin_viewer && puffin_viewer --url 127.0.0.1:8585");
std::process::Command::new("puffin_viewer")
.arg("--url")
.arg("127.0.0.1:8585")
.spawn()
.ok();
// We can store the server if we want, but in this case we just want
// it to keep running. Dropping it closes the server, so let's not drop it!
#[allow(clippy::mem_forget)]

View File

@@ -1,7 +0,0 @@
Example how to show an image with eframe/egui.
```sh
cargo run -p retained_image
```
![](screenshot.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -1,84 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
use egui_extras::RetainedImage;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(400.0, 1000.0)),
..Default::default()
};
eframe::run_native(
"Show an image with eframe/egui",
options,
Box::new(|_cc| Box::<MyApp>::default()),
)
}
struct MyApp {
image: RetainedImage,
rounding: f32,
tint: egui::Color32,
}
impl Default for MyApp {
fn default() -> Self {
Self {
// crab image is CC0, found on https://stocksnap.io/search/crab
image: RetainedImage::from_image_bytes("crab.png", include_bytes!("crab.png")).unwrap(),
rounding: 32.0,
tint: egui::Color32::from_rgb(100, 200, 200),
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let Self {
image,
rounding,
tint,
} = self;
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("This is an image:");
image.show(ui);
ui.add_space(32.0);
ui.heading("This is a tinted image with rounded corners:");
ui.add(
egui::Image::new(image.texture_id(ctx), image.size_vec2())
.tint(*tint)
.rounding(*rounding),
);
ui.horizontal(|ui| {
ui.label("Tint:");
egui::color_picker::color_edit_button_srgba(
ui,
tint,
egui::color_picker::Alpha::BlendOrAdditive,
);
ui.add_space(16.0);
ui.label("Rounding:");
ui.add(
egui::DragValue::new(rounding)
.speed(1.0)
.clamp_range(0.0..=0.5 * image.size_vec2().min_elem()),
);
});
ui.add_space(32.0);
ui.heading("This is an image you can click:");
ui.add(egui::ImageButton::new(
image.texture_id(ctx),
image.size_vec2(),
));
});
}
}

View File

@@ -63,7 +63,7 @@ impl eframe::App for MyApp {
});
if let Some(texture) = self.texture.as_ref() {
ui.image(texture, ui.available_size());
ui.image((texture.id(), ui.available_size()));
} else {
ui.spinner();
}

View File

@@ -1,16 +0,0 @@
[package]
name = "svg"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.70"
publish = false
[dependencies]
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 = ["log", "svg"] }
env_logger = "0.10"

View File

@@ -1,7 +0,0 @@
Example how to show an SVG image.
```sh
cargo run -p svg
```
![](screenshot.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -1,47 +0,0 @@
//! A good way of displaying an SVG image in egui.
//!
//! Requires the dependency `egui_extras` with the `svg` feature.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(1000.0, 700.0)),
..Default::default()
};
eframe::run_native(
"svg example",
options,
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;
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("SVG example");
ui.label("The SVG is rasterized and displayed as a texture.");
ui.separator();
let max_size = ui.available_size();
ui.add(
egui::Image2::from_static_bytes(
"ferris.svg",
include_bytes!("rustacean-flat-happy.svg"),
)
.size_hint(max_size),
);
});
}
}

View File

@@ -1 +0,0 @@
Rust logo by Mozilla, from https://github.com/rust-lang/rust-artwork