Merge branch 'master' of https://github.com/emilk/egui into multiples_viewports
13
.github/workflows/rust.yml
vendored
@@ -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
@@ -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"
|
||||
|
||||
50
README.md
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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},
|
||||
|
||||
BIN
crates/egui/assets/ferris.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
69
crates/egui/src/load/bytes_loader.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
60
crates/egui/src/load/texture_loader.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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(¤t_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)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
214
crates/egui_demo_app/src/apps/image_viewer.rs
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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]
|
||||
|
||||
BIN
crates/egui_demo_lib/assets/icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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"] }
|
||||
@@ -1,7 +0,0 @@
|
||||
Example how to download and show an image with eframe/egui.
|
||||
|
||||
```sh
|
||||
cargo run -p download_image
|
||||
```
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 353 KiB |
@@ -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",
|
||||
))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
] }
|
||||
7
examples/images/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Example showing how to show images with eframe/egui.
|
||||
|
||||
```sh
|
||||
cargo run -p images
|
||||
```
|
||||
|
||||

|
||||
BIN
examples/images/screenshot.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
38
examples/images/src/main.rs
Normal 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)),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Example how to show an image with eframe/egui.
|
||||
|
||||
```sh
|
||||
cargo run -p retained_image
|
||||
```
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 140 KiB |
@@ -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(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -1,7 +0,0 @@
|
||||
Example how to show an SVG image.
|
||||
|
||||
```sh
|
||||
cargo run -p svg
|
||||
```
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 142 KiB |
@@ -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),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
Rust logo by Mozilla, from https://github.com/rust-lang/rust-artwork
|
||||