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
|
run: cargo check --locked --all-features --all-targets
|
||||||
|
|
||||||
- name: check egui_extras --all-features
|
- 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
|
- name: check default features
|
||||||
run: cargo check --locked --all-targets
|
run: cargo check --locked --all-targets
|
||||||
@@ -57,11 +57,14 @@ jobs:
|
|||||||
- name: check --no-default-features
|
- name: check --no-default-features
|
||||||
run: cargo check --locked --no-default-features --lib --all-targets
|
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
|
- 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
|
- name: Test doc-tests
|
||||||
run: cargo test --doc --all-features
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
|
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "download_image"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"eframe",
|
|
||||||
"egui_extras",
|
|
||||||
"env_logger",
|
|
||||||
"image",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dyn-clonable"
|
name = "dyn-clonable"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -1232,6 +1222,7 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
"poll-promise",
|
"poll-promise",
|
||||||
|
"rfd",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
@@ -1248,10 +1239,8 @@ dependencies = [
|
|||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"egui_plot",
|
"egui_plot",
|
||||||
"enum-map",
|
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"syntect",
|
|
||||||
"unicode_names2",
|
"unicode_names2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1263,11 +1252,14 @@ dependencies = [
|
|||||||
"document-features",
|
"document-features",
|
||||||
"egui",
|
"egui",
|
||||||
"ehttp",
|
"ehttp",
|
||||||
|
"enum-map",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
|
"mime_guess",
|
||||||
"puffin",
|
"puffin",
|
||||||
"resvg",
|
"resvg",
|
||||||
"serde",
|
"serde",
|
||||||
|
"syntect",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
"usvg",
|
"usvg",
|
||||||
]
|
]
|
||||||
@@ -2027,6 +2019,16 @@ dependencies = [
|
|||||||
"png",
|
"png",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "images"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"eframe",
|
||||||
|
"egui_extras",
|
||||||
|
"env_logger",
|
||||||
|
"image",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "imagesize"
|
name = "imagesize"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -2311,6 +2313,22 @@ dependencies = [
|
|||||||
"paste",
|
"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]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -2999,16 +3017,6 @@ dependencies = [
|
|||||||
"usvg",
|
"usvg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "retained_image"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"eframe",
|
|
||||||
"egui_extras",
|
|
||||||
"env_logger",
|
|
||||||
"image",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfd"
|
name = "rfd"
|
||||||
version = "0.11.4"
|
version = "0.11.4"
|
||||||
@@ -3409,15 +3417,6 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "svg"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"eframe",
|
|
||||||
"egui_extras",
|
|
||||||
"env_logger",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "svgtypes"
|
name = "svgtypes"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -3748,6 +3747,15 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.13"
|
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!
|
Missing an integration for the thing you're working on? Create one, it's easy!
|
||||||
|
|
||||||
### Writing your own egui integration
|
### Writing your own egui integration
|
||||||
|
See <https://docs.rs/egui/latest/egui/#integrating-with-egui>.
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
## Why immediate mode
|
## Why immediate mode
|
||||||
|
|||||||
@@ -60,4 +60,4 @@ Not all rust crates work when compiled to WASM, but here are some useful crates
|
|||||||
|
|
||||||
|
|
||||||
## Name
|
## 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.
|
/// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key.
|
||||||
#[cfg(feature = "ron")]
|
#[cfg(feature = "ron")]
|
||||||
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
|
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
|
||||||
|
crate::profile_function!(key);
|
||||||
storage
|
storage
|
||||||
.get_string(key)
|
.get_string(key)
|
||||||
.and_then(|value| match ron::from_str(&value) {
|
.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.
|
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
|
||||||
#[cfg(feature = "ron")]
|
#[cfg(feature = "ron")]
|
||||||
pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, value: &T) {
|
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) {
|
match ron::ser::to_string(value) {
|
||||||
Ok(string) => storage.set_string(key, string),
|
Ok(string) => storage.set_string(key, string),
|
||||||
Err(err) => log::error!("eframe failed to encode data using ron: {}", err),
|
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 {
|
mod profiling_scopes {
|
||||||
#![allow(unused_macros)]
|
#![allow(unused_macros)]
|
||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
@@ -333,6 +332,7 @@ mod profiling_scopes {
|
|||||||
macro_rules! profile_function {
|
macro_rules! profile_function {
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
|
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
||||||
puffin::profile_function!($($arg)*);
|
puffin::profile_function!($($arg)*);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -342,11 +342,12 @@ mod profiling_scopes {
|
|||||||
macro_rules! profile_scope {
|
macro_rules! profile_scope {
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
|
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
||||||
puffin::profile_scope!($($arg)*);
|
puffin::profile_scope!($($arg)*);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use profile_scope;
|
pub(crate) use profile_scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[allow(unused_imports)]
|
||||||
pub(crate) use profiling_scopes::*;
|
pub(crate) use profiling_scopes::*;
|
||||||
|
|||||||
@@ -191,6 +191,8 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
|
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
appkit::{NSApp, NSApplication, NSImage, NSMenu, NSWindow},
|
appkit::{NSApp, NSApplication, NSImage, NSMenu, NSWindow},
|
||||||
base::{id, nil},
|
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,
|
png_bytes.len() as u64,
|
||||||
);
|
);
|
||||||
let app_icon = NSImage::initWithData_(NSImage::alloc(nil), data);
|
let app_icon = NSImage::initWithData_(NSImage::alloc(nil), data);
|
||||||
|
|
||||||
|
crate::profile_scope!("setApplicationIconImage_");
|
||||||
app.setApplicationIconImage_(app_icon);
|
app.setApplicationIconImage_(app_icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the title in the top bar - for python processes this would be again "python" otherwise.
|
// Change the title in the top bar - for python processes this would be again "python" otherwise.
|
||||||
let main_menu = app.mainMenu();
|
let main_menu = app.mainMenu();
|
||||||
let app_menu: id = msg_send![main_menu.itemAtIndex_(0), submenu];
|
let app_menu: id = msg_send![main_menu.itemAtIndex_(0), submenu];
|
||||||
|
crate::profile_scope!("setTitle_");
|
||||||
app_menu.setTitle_(NSString::alloc(nil).init_str(title));
|
app_menu.setTitle_(NSString::alloc(nil).init_str(title));
|
||||||
|
|
||||||
// The title in the Dock apparently can't be changed.
|
// 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,
|
native_options: &crate::NativeOptions,
|
||||||
window_settings: Option<WindowSettings>,
|
window_settings: Option<WindowSettings>,
|
||||||
) {
|
) {
|
||||||
|
crate::profile_function!();
|
||||||
use winit::window::WindowLevel;
|
use winit::window::WindowLevel;
|
||||||
window.set_window_level(if native_options.always_on_top {
|
window.set_window_level(if native_options.always_on_top {
|
||||||
WindowLevel::AlwaysOnTop
|
WindowLevel::AlwaysOnTop
|
||||||
@@ -443,6 +444,8 @@ impl EpiIntegration {
|
|||||||
egui_winit: &mut egui_winit::State,
|
egui_winit: &mut egui_winit::State,
|
||||||
viewport_id: ViewportId,
|
viewport_id: ViewportId,
|
||||||
) -> EventResponse {
|
) -> EventResponse {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
@@ -614,6 +617,7 @@ const STORAGE_EGUI_MEMORY_KEY: &str = "egui";
|
|||||||
const STORAGE_WINDOW_KEY: &str = "window";
|
const STORAGE_WINDOW_KEY: &str = "window";
|
||||||
|
|
||||||
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
|
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
|
||||||
|
crate::profile_function!();
|
||||||
#[cfg(feature = "persistence")]
|
#[cfg(feature = "persistence")]
|
||||||
{
|
{
|
||||||
epi::get_value(_storage?, STORAGE_WINDOW_KEY)
|
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> {
|
pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option<egui::Memory> {
|
||||||
|
crate::profile_function!();
|
||||||
#[cfg(feature = "persistence")]
|
#[cfg(feature = "persistence")]
|
||||||
{
|
{
|
||||||
epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)
|
epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ pub struct FileStorage {
|
|||||||
impl Drop for FileStorage {
|
impl Drop for FileStorage {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(join_handle) = self.last_save_join_handle.take() {
|
if let Some(join_handle) = self.last_save_join_handle.take() {
|
||||||
|
crate::profile_scope!("wait_for_save");
|
||||||
join_handle.join().ok();
|
join_handle.join().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,6 +41,7 @@ impl Drop for FileStorage {
|
|||||||
impl FileStorage {
|
impl FileStorage {
|
||||||
/// Store the state in this .ron file.
|
/// Store the state in this .ron file.
|
||||||
fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
|
||||||
|
crate::profile_function!();
|
||||||
let ron_filepath: PathBuf = ron_filepath.into();
|
let ron_filepath: PathBuf = ron_filepath.into();
|
||||||
log::debug!("Loading app state from {:?}…", ron_filepath);
|
log::debug!("Loading app state from {:?}…", ron_filepath);
|
||||||
Self {
|
Self {
|
||||||
@@ -51,6 +54,7 @@ impl FileStorage {
|
|||||||
|
|
||||||
/// Find a good place to put the files that the OS likes.
|
/// Find a good place to put the files that the OS likes.
|
||||||
pub fn from_app_id(app_id: &str) -> Option<Self> {
|
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 Some(data_dir) = storage_dir(app_id) {
|
||||||
if let Err(err) = std::fs::create_dir_all(&data_dir) {
|
if let Err(err) = std::fs::create_dir_all(&data_dir) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
@@ -83,6 +87,7 @@ impl crate::Storage for FileStorage {
|
|||||||
|
|
||||||
fn flush(&mut self) {
|
fn flush(&mut self) {
|
||||||
if self.dirty {
|
if self.dirty {
|
||||||
|
crate::profile_function!();
|
||||||
self.dirty = false;
|
self.dirty = false;
|
||||||
|
|
||||||
let file_path = self.ron_filepath.clone();
|
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) {
|
match std::fs::File::create(file_path) {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
|
let mut writer = std::io::BufWriter::new(file);
|
||||||
let config = Default::default();
|
let config = Default::default();
|
||||||
|
|
||||||
if let Err(err) = ron::ser::to_writer_pretty(file, &kv, config) {
|
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
|
||||||
log::warn!("Failed to serialize app state: {err}");
|
.and_then(|_| writer.flush().map_err(|err| err.into()))
|
||||||
|
{
|
||||||
|
log::warn!("Failed to serialize app state: {}", err);
|
||||||
} else {
|
} else {
|
||||||
log::trace!("Persisted to {:?}", file_path);
|
log::trace!("Persisted to {:?}", file_path);
|
||||||
}
|
}
|
||||||
@@ -142,6 +150,7 @@ fn read_ron<T>(ron_path: impl AsRef<Path>) -> Option<T>
|
|||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned,
|
T: serde::de::DeserializeOwned,
|
||||||
{
|
{
|
||||||
|
crate::profile_function!();
|
||||||
match std::fs::File::open(ron_path) {
|
match std::fs::File::open(ron_path) {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
let reader = std::io::BufReader::new(file);
|
let reader = std::io::BufReader::new(file);
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ trait WinitApp {
|
|||||||
fn create_event_loop_builder(
|
fn create_event_loop_builder(
|
||||||
native_options: &mut epi::NativeOptions,
|
native_options: &mut epi::NativeOptions,
|
||||||
) -> EventLoopBuilder<UserEvent> {
|
) -> EventLoopBuilder<UserEvent> {
|
||||||
|
crate::profile_function!();
|
||||||
let mut event_loop_builder = winit::event_loop::EventLoopBuilder::with_user_event();
|
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) {
|
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
|
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.
|
/// Access a thread-local event loop.
|
||||||
///
|
///
|
||||||
/// We reuse the event-loop so we can support closing and opening an eframe window
|
/// 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
|
// do that as part of the lazy thread local storage initialization and so we instead
|
||||||
// create the event loop lazily here
|
// create the event loop lazily here
|
||||||
let mut event_loop = event_loop.borrow_mut();
|
let mut event_loop = event_loop.borrow_mut();
|
||||||
let event_loop = event_loop
|
let event_loop = event_loop.get_or_insert_with(|| create_event_loop(&mut native_options));
|
||||||
.get_or_insert_with(|| create_event_loop_builder(&mut native_options).build());
|
|
||||||
f(event_loop, native_options)
|
f(event_loop, native_options)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -165,6 +173,8 @@ fn run_and_return(
|
|||||||
let mut returned_result = Ok(());
|
let mut returned_result = Ok(());
|
||||||
|
|
||||||
event_loop.run_return(|event, event_loop, control_flow| {
|
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);
|
WINIT_EVENT_LOOP.with(|row_event_loop| *row_event_loop.write() = event_loop);
|
||||||
|
|
||||||
let events = match &event {
|
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();
|
let mut windows_next_repaint_times = HashMap::default();
|
||||||
|
|
||||||
event_loop.run(move |event, event_loop, control_flow| {
|
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);
|
WINIT_EVENT_LOOP.with(|row_event_loop| *row_event_loop.write() = event_loop);
|
||||||
|
|
||||||
let events = match event {
|
let events = match event {
|
||||||
@@ -536,6 +548,8 @@ mod glow_integration {
|
|||||||
native_options: &epi::NativeOptions,
|
native_options: &epi::NativeOptions,
|
||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
use glutin::prelude::*;
|
use glutin::prelude::*;
|
||||||
// convert native options to glutin options
|
// convert native options to glutin options
|
||||||
let hardware_acceleration = match native_options.hardware_acceleration {
|
let hardware_acceleration = match native_options.hardware_acceleration {
|
||||||
@@ -576,26 +590,35 @@ mod glow_integration {
|
|||||||
"trying to create glutin Display with config: {:?}",
|
"trying to create glutin Display with config: {:?}",
|
||||||
&config_template_builder
|
&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.
|
// 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_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150
|
||||||
.with_window_builder(Some(create_winit_window_builder(&window_builder)))
|
.with_window_builder(Some(create_winit_window_builder(&window_builder)));
|
||||||
.build(
|
|
||||||
event_loop,
|
let (window, gl_config) = {
|
||||||
config_template_builder.clone(),
|
crate::profile_scope!("DisplayBuilder::build");
|
||||||
|mut config_iterator| {
|
|
||||||
let config = config_iterator.next().expect(
|
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",
|
"failed to find a matching configuration for creating glutin config",
|
||||||
);
|
);
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"using the first config from config picker closure. config: {:?}",
|
"using the first config from config picker closure. config: {:?}",
|
||||||
&config
|
&config
|
||||||
);
|
);
|
||||||
config
|
config
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))?;
|
.map_err(|e| {
|
||||||
|
crate::Error::NoGlutinConfigs(config_template_builder.build(), e)
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
let gl_display = gl_config.display();
|
let gl_display = gl_config.display();
|
||||||
log::debug!(
|
log::debug!(
|
||||||
@@ -615,10 +638,15 @@ mod glow_integration {
|
|||||||
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new()
|
let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new()
|
||||||
.with_context_api(glutin::context::ContextApi::Gles(None))
|
.with_context_api(glutin::context::ContextApi::Gles(None))
|
||||||
.build(raw_window_handle);
|
.build(raw_window_handle);
|
||||||
let gl_context = match gl_config
|
|
||||||
.display()
|
let gl_context_result = {
|
||||||
.create_context(&gl_config, &context_attributes)
|
crate::profile_scope!("create_context");
|
||||||
{
|
gl_config
|
||||||
|
.display()
|
||||||
|
.create_context(&gl_config, &context_attributes)
|
||||||
|
};
|
||||||
|
|
||||||
|
let gl_context = match gl_context_result {
|
||||||
Ok(it) => it,
|
Ok(it) => it,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::warn!("failed to create context using default context attributes {context_attributes:?} due to error: {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
|
/// we presently assume that we will
|
||||||
fn on_resume(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
|
fn on_resume(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
let values = self
|
let values = self
|
||||||
.windows
|
.windows
|
||||||
.values()
|
.values()
|
||||||
@@ -849,6 +879,7 @@ mod glow_integration {
|
|||||||
native_options: epi::NativeOptions,
|
native_options: epi::NativeOptions,
|
||||||
app_creator: epi::AppCreator,
|
app_creator: epi::AppCreator,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
crate::profile_function!();
|
||||||
Self {
|
Self {
|
||||||
repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())),
|
repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())),
|
||||||
app_name: app_name.to_owned(),
|
app_name: app_name.to_owned(),
|
||||||
@@ -889,6 +920,7 @@ mod glow_integration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let gl = unsafe {
|
let gl = unsafe {
|
||||||
|
crate::profile_scope!("glow::Context::from_loader_function");
|
||||||
glow::Context::from_loader_function(|s| {
|
glow::Context::from_loader_function(|s| {
|
||||||
let s = std::ffi::CString::new(s)
|
let s = std::ffi::CString::new(s)
|
||||||
.expect("failed to construct C string from string for gl proc address");
|
.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<()> {
|
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
|
||||||
|
crate::profile_function!();
|
||||||
let storage = epi_integration::create_storage(
|
let storage = epi_integration::create_storage(
|
||||||
self.native_options
|
self.native_options
|
||||||
.app_id
|
.app_id
|
||||||
@@ -960,14 +993,6 @@ mod glow_integration {
|
|||||||
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
||||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
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 {
|
if self.native_options.mouse_passthrough {
|
||||||
gl_window
|
gl_window
|
||||||
.window(ViewportId::MAIN)
|
.window(ViewportId::MAIN)
|
||||||
@@ -1286,7 +1311,10 @@ mod glow_integration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save_and_destroy(&mut self) {
|
fn save_and_destroy(&mut self) {
|
||||||
|
crate::profile_function!();
|
||||||
if let Some(running) = self.running.write().take() {
|
if let Some(running) = self.running.write().take() {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
running.integration.write().save(
|
running.integration.write().save(
|
||||||
running.app.write().as_mut(),
|
running.app.write().as_mut(),
|
||||||
running
|
running
|
||||||
@@ -1543,6 +1571,8 @@ mod glow_integration {
|
|||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
event: &winit::event::Event<'_, UserEvent>,
|
event: &winit::event::Event<'_, UserEvent>,
|
||||||
) -> Result<EventResult> {
|
) -> Result<EventResult> {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
Ok(match event {
|
Ok(match event {
|
||||||
winit::event::Event::Resumed => {
|
winit::event::Event::Resumed => {
|
||||||
// first resume event.
|
// first resume event.
|
||||||
@@ -1632,7 +1662,7 @@ mod glow_integration {
|
|||||||
// See: https://github.com/rust-windowing/winit/issues/208
|
// See: https://github.com/rust-windowing/winit/issues/208
|
||||||
// This solves an issue where the app would panic when minimizing on Windows.
|
// This solves an issue where the app would panic when minimizing on Windows.
|
||||||
let glutin_ctx = &mut *running.glutin_ctx.write();
|
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) {
|
if let Some(id) = glutin_ctx.window_maps.get(window_id) {
|
||||||
glutin_ctx.resize(*id, *physical_size);
|
glutin_ctx.resize(*id, *physical_size);
|
||||||
}
|
}
|
||||||
@@ -1723,11 +1753,14 @@ mod glow_integration {
|
|||||||
EventResult::Wait
|
EventResult::Wait
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
|
winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest(
|
||||||
accesskit_winit::ActionRequestEvent { request, window_id },
|
accesskit_winit::ActionRequestEvent { request, window_id },
|
||||||
)) => {
|
)) => {
|
||||||
if let Some(running) = self.running.read().as_ref() {
|
if let Some(running) = self.running.read().as_ref() {
|
||||||
|
crate::profile_scope!("on_accesskit_action_request");
|
||||||
|
|
||||||
let glutin_ctx = running.glutin_ctx.read();
|
let glutin_ctx = running.glutin_ctx.read();
|
||||||
if let Some(viewport_id) = glutin_ctx.window_maps.get(window_id).copied() {
|
if let Some(viewport_id) = glutin_ctx.window_maps.get(window_id).copied() {
|
||||||
if let Some(viewport) = glutin_ctx.windows.get(&viewport_id).cloned() {
|
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)
|
run_and_return(event_loop, glow_eframe)
|
||||||
})
|
})
|
||||||
} else {
|
} 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);
|
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||||
run_and_exit(event_loop, glow_eframe);
|
run_and_exit(event_loop, glow_eframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
#[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);
|
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||||
run_and_exit(event_loop, glow_eframe);
|
run_and_exit(event_loop, glow_eframe);
|
||||||
}
|
}
|
||||||
@@ -1847,6 +1880,7 @@ mod wgpu_integration {
|
|||||||
native_options: epi::NativeOptions,
|
native_options: epi::NativeOptions,
|
||||||
app_creator: epi::AppCreator,
|
app_creator: epi::AppCreator,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
crate::profile_function!();
|
||||||
#[cfg(feature = "__screenshot")]
|
#[cfg(feature = "__screenshot")]
|
||||||
assert!(
|
assert!(
|
||||||
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
|
||||||
@@ -1870,10 +1904,15 @@ mod wgpu_integration {
|
|||||||
native_options: &NativeOptions,
|
native_options: &NativeOptions,
|
||||||
) -> std::result::Result<(winit::window::Window, ViewportBuilder), winit::error::OsError>
|
) -> std::result::Result<(winit::window::Window, ViewportBuilder), winit::error::OsError>
|
||||||
{
|
{
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
let window_settings = epi_integration::load_window_settings(storage);
|
let window_settings = epi_integration::load_window_settings(storage);
|
||||||
let window_builder =
|
let window_builder =
|
||||||
epi_integration::window_builder(event_loop, title, native_options, window_settings);
|
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(
|
epi_integration::apply_native_options_to_window(
|
||||||
&window,
|
&window,
|
||||||
native_options,
|
native_options,
|
||||||
@@ -1935,6 +1974,7 @@ mod wgpu_integration {
|
|||||||
|
|
||||||
fn set_window(&mut self, id: ViewportId) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
fn set_window(&mut self, id: ViewportId) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
||||||
if let Some(running) = &mut self.running {
|
if let Some(running) = &mut self.running {
|
||||||
|
crate::profile_function!();
|
||||||
if let Some(Window { window, .. }) = running.windows.read().get(&id) {
|
if let Some(Window { window, .. }) = running.windows.read().get(&id) {
|
||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
if let Some(win) = &window {
|
if let Some(win) = &window {
|
||||||
@@ -1966,6 +2006,8 @@ mod wgpu_integration {
|
|||||||
window: winit::window::Window,
|
window: winit::window::Window,
|
||||||
builder: ViewportBuilder,
|
builder: ViewportBuilder,
|
||||||
) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
) -> std::result::Result<(), egui_wgpu::WgpuError> {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
#[allow(unsafe_code, unused_mut, unused_unsafe)]
|
||||||
let mut painter = egui_wgpu::winit::Painter::new(
|
let mut painter = egui_wgpu::winit::Painter::new(
|
||||||
self.native_options.wgpu_options.clone(),
|
self.native_options.wgpu_options.clone(),
|
||||||
@@ -2001,8 +2043,6 @@ mod wgpu_integration {
|
|||||||
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
let theme = system_theme.unwrap_or(self.native_options.default_theme);
|
||||||
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
integration.egui_ctx.set_visuals(theme.egui_visuals());
|
||||||
|
|
||||||
window.set_ime_allowed(true);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let event_loop_proxy = self.repaint_proxy.clone();
|
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)
|
let app_creator = std::mem::take(&mut self.app_creator)
|
||||||
.expect("Single-use AppCreator has unexpectedly already been taken");
|
.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(),
|
egui_ctx: integration.egui_ctx.clone(),
|
||||||
integration_info: integration.frame.info().clone(),
|
integration_info: integration.frame.info().clone(),
|
||||||
storage: integration.frame.storage(),
|
storage: integration.frame.storage(),
|
||||||
@@ -2035,7 +2075,11 @@ mod wgpu_integration {
|
|||||||
wgpu_render_state,
|
wgpu_render_state,
|
||||||
raw_display_handle: window.raw_display_handle(),
|
raw_display_handle: window.raw_display_handle(),
|
||||||
raw_window_handle: window.raw_window_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() {
|
if app.warm_up_enabled() {
|
||||||
integration.warm_up(app.as_mut(), &window, &mut state);
|
integration.warm_up(app.as_mut(), &window, &mut state);
|
||||||
@@ -2189,6 +2233,7 @@ mod wgpu_integration {
|
|||||||
|
|
||||||
fn save_and_destroy(&mut self) {
|
fn save_and_destroy(&mut self) {
|
||||||
if let Some(mut running) = self.running.take() {
|
if let Some(mut running) = self.running.take() {
|
||||||
|
crate::profile_function!();
|
||||||
if let Some(Window { window, .. }) = running.windows.read().get(&ViewportId::MAIN) {
|
if let Some(Window { window, .. }) = running.windows.read().get(&ViewportId::MAIN) {
|
||||||
running
|
running
|
||||||
.integration
|
.integration
|
||||||
@@ -2403,7 +2448,9 @@ mod wgpu_integration {
|
|||||||
event_loop: &EventLoopWindowTarget<UserEvent>,
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
||||||
event: &winit::event::Event<'_, UserEvent>,
|
event: &winit::event::Event<'_, UserEvent>,
|
||||||
) -> Result<EventResult> {
|
) -> Result<EventResult> {
|
||||||
|
crate::profile_function!();
|
||||||
self.build_windows(event_loop);
|
self.build_windows(event_loop);
|
||||||
|
|
||||||
Ok(match event {
|
Ok(match event {
|
||||||
winit::event::Event::Resumed => {
|
winit::event::Event::Resumed => {
|
||||||
if let Some(running) = &self.running {
|
if let Some(running) = &self.running {
|
||||||
@@ -2484,7 +2531,7 @@ mod wgpu_integration {
|
|||||||
if let Some(viewport_id) =
|
if let Some(viewport_id) =
|
||||||
running.windows_id.read().get(window_id).copied()
|
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(
|
running.painter.write().on_window_resized(
|
||||||
viewport_id,
|
viewport_id,
|
||||||
physical_size.width,
|
physical_size.width,
|
||||||
@@ -2600,14 +2647,14 @@ mod wgpu_integration {
|
|||||||
run_and_return(event_loop, wgpu_eframe)
|
run_and_return(event_loop, wgpu_eframe)
|
||||||
})
|
})
|
||||||
} else {
|
} 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);
|
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||||
run_and_exit(event_loop, wgpu_eframe);
|
run_and_exit(event_loop, wgpu_eframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
#[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);
|
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
|
||||||
run_and_exit(event_loop, wgpu_eframe);
|
run_and_exit(event_loop, wgpu_eframe);
|
||||||
}
|
}
|
||||||
@@ -2628,3 +2675,67 @@ fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Opti
|
|||||||
None
|
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.
|
// When input lost focus, focus on it again.
|
||||||
// It is useful when user click somewhere outside canvas.
|
// 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, _| {
|
runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| {
|
||||||
// Delay 10 ms, and focus again.
|
// Delay 10 ms, and focus again.
|
||||||
let func = js_sys::Function::new_no_args(&format!(
|
let input_refocus = input_refocus.clone();
|
||||||
"document.getElementById('{AGENT_ID}').focus()"
|
call_after_delay(std::time::Duration::from_millis(10), move || {
|
||||||
));
|
input_refocus.focus().ok();
|
||||||
window
|
});
|
||||||
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
|
|
||||||
.unwrap();
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
body.append_child(&input)?;
|
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"
|
mod profiling_scopes {
|
||||||
macro_rules! profile_function {
|
#![allow(unused_macros)]
|
||||||
($($arg: tt)*) => {
|
#![allow(unused_imports)]
|
||||||
#[cfg(feature = "puffin")]
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
puffin::profile_function!($($arg)*);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
pub(crate) use profile_function;
|
|
||||||
|
|
||||||
/// Profiling macro for feature "puffin"
|
/// Profiling macro for feature "puffin"
|
||||||
macro_rules! profile_scope {
|
macro_rules! profile_function {
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
||||||
puffin::profile_scope!($($arg)*);
|
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;
|
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
|
// 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
|
// 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
|
// viewport during the paint callback, effectively overriding this
|
||||||
// one.
|
// one.
|
||||||
|
|
||||||
let min = (callback.rect.min.to_vec2() * pixels_per_point).round();
|
let viewport_px = info.viewport_in_pixels();
|
||||||
let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
|
|
||||||
|
|
||||||
render_pass.set_viewport(
|
render_pass.set_viewport(
|
||||||
min.x,
|
viewport_px.left_px,
|
||||||
min.y,
|
viewport_px.top_px,
|
||||||
max.x - min.x,
|
viewport_px.width_px,
|
||||||
max.y - min.y,
|
viewport_px.height_px,
|
||||||
0.0,
|
0.0,
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cbfn.0.paint(
|
cbfn.0.paint(info, render_pass, &self.callback_resources);
|
||||||
PaintCallbackInfo {
|
|
||||||
viewport: callback.rect,
|
|
||||||
clip_rect: *clip_rect,
|
|
||||||
pixels_per_point,
|
|
||||||
screen_size_px: size_in_pixels,
|
|
||||||
},
|
|
||||||
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.
|
/// 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
|
/// This could be used by custom paint hooks to render images that have been added through
|
||||||
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
|
/// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
|
||||||
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
|
|
||||||
pub fn texture(
|
pub fn texture(
|
||||||
&self,
|
&self,
|
||||||
id: &epaint::TextureId,
|
id: &epaint::TextureId,
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ impl Painter {
|
|||||||
render_state: &RenderState,
|
render_state: &RenderState,
|
||||||
present_mode: wgpu::PresentMode,
|
present_mode: wgpu::PresentMode,
|
||||||
) {
|
) {
|
||||||
|
crate::profile_function!();
|
||||||
let usage = if surface_state.supports_screenshot {
|
let usage = if surface_state.supports_screenshot {
|
||||||
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST
|
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST
|
||||||
} else {
|
} else {
|
||||||
@@ -188,6 +189,8 @@ impl Painter {
|
|||||||
viewport_id: ViewportId,
|
viewport_id: ViewportId,
|
||||||
window: Option<&winit::window::Window>,
|
window: Option<&winit::window::Window>,
|
||||||
) -> Result<(), crate::WgpuError> {
|
) -> Result<(), crate::WgpuError> {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
let size = window.inner_size();
|
let size = window.inner_size();
|
||||||
if self.surfaces.get(&viewport_id).is_none() {
|
if self.surfaces.get(&viewport_id).is_none() {
|
||||||
@@ -269,6 +272,7 @@ impl Painter {
|
|||||||
width_in_pixels: u32,
|
width_in_pixels: u32,
|
||||||
height_in_pixels: u32,
|
height_in_pixels: u32,
|
||||||
) {
|
) {
|
||||||
|
crate::profile_function!();
|
||||||
let render_state = self.render_state.as_ref().unwrap();
|
let render_state = self.render_state.as_ref().unwrap();
|
||||||
let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
|
let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
|
||||||
|
|
||||||
@@ -329,6 +333,8 @@ impl Painter {
|
|||||||
width_in_pixels: u32,
|
width_in_pixels: u32,
|
||||||
height_in_pixels: u32,
|
height_in_pixels: u32,
|
||||||
) {
|
) {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
if self.surfaces.get(&viewport_id).is_some() {
|
if self.surfaces.get(&viewport_id).is_some() {
|
||||||
self.resize_and_generate_depth_texture_view_and_msaa_view(
|
self.resize_and_generate_depth_texture_view_and_msaa_view(
|
||||||
viewport_id,
|
viewport_id,
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ pub struct State {
|
|||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
accesskit: Option<accesskit_winit::Adapter>,
|
accesskit: Option<accesskit_winit::Adapter>,
|
||||||
|
|
||||||
|
allow_ime: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
@@ -110,6 +112,8 @@ impl State {
|
|||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
accesskit: None,
|
accesskit: None,
|
||||||
|
|
||||||
|
allow_ime: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +231,8 @@ impl State {
|
|||||||
egui_ctx: &egui::Context,
|
egui_ctx: &egui::Context,
|
||||||
event: &winit::event::WindowEvent<'_>,
|
event: &winit::event::WindowEvent<'_>,
|
||||||
) -> EventResponse {
|
) -> EventResponse {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
use winit::event::WindowEvent;
|
use winit::event::WindowEvent;
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||||
@@ -706,6 +712,12 @@ impl State {
|
|||||||
self.clipboard.set(copied_text);
|
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 {
|
if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
|
||||||
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
|
window.set_ime_position(winit::dpi::LogicalPosition { x, y });
|
||||||
}
|
}
|
||||||
@@ -1343,29 +1355,33 @@ pub fn changes_between_builders(
|
|||||||
}
|
}
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Profiling macro for feature "puffin"
|
mod profiling_scopes {
|
||||||
#[allow(unused_macros)]
|
#![allow(unused_macros)]
|
||||||
macro_rules! profile_function {
|
#![allow(unused_imports)]
|
||||||
($($arg: tt)*) => {
|
|
||||||
#[cfg(feature = "puffin")]
|
/// Profiling macro for feature "puffin"
|
||||||
puffin::profile_function!($($arg)*);
|
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)]
|
#[allow(unused_imports)]
|
||||||
pub(crate) use profile_function;
|
pub(crate) use profiling_scopes::*;
|
||||||
|
|
||||||
/// 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;
|
|
||||||
use winit::{
|
use winit::{
|
||||||
dpi::{LogicalPosition, LogicalSize},
|
dpi::{LogicalPosition, LogicalSize},
|
||||||
window::{CursorGrabMode, WindowButtons, WindowLevel},
|
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.
|
#![warn(missing_docs)] // Let's keep `Context` well-documented.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::load::Bytes;
|
||||||
|
use crate::load::SizedTexture;
|
||||||
use crate::{
|
use crate::{
|
||||||
animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState,
|
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, *,
|
output::FullOutput, util::IdTypeMap, TextureHandle, ViewportCommand, *,
|
||||||
};
|
};
|
||||||
use ahash::HashMap;
|
use ahash::HashMap;
|
||||||
@@ -228,7 +231,7 @@ struct ContextImpl {
|
|||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
accesskit_node_classes: accesskit::NodeClassSet,
|
accesskit_node_classes: accesskit::NodeClassSet,
|
||||||
|
|
||||||
loaders: load::Loaders,
|
loaders: Arc<Loaders>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextImpl {
|
impl ContextImpl {
|
||||||
@@ -1293,17 +1296,24 @@ impl Context {
|
|||||||
self.options(|opt| opt.style.clone())
|
self.options(|opt| opt.style.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`Style`] used by all new windows, panels etc.
|
/// Mutate the [`Style`] used by all subsequent windows, panels etc.
|
||||||
///
|
|
||||||
/// You can also use [`Ui::style_mut`] to change the style of a single [`Ui`].
|
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```
|
/// ```
|
||||||
/// # let mut ctx = egui::Context::default();
|
/// # let mut ctx = egui::Context::default();
|
||||||
/// let mut style: egui::Style = (*ctx.style()).clone();
|
/// ctx.style_mut(|style| {
|
||||||
/// style.spacing.item_spacing = egui::vec2(10.0, 20.0);
|
/// style.spacing.item_spacing = egui::vec2(10.0, 20.0);
|
||||||
/// ctx.set_style(style);
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
|
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>>) {
|
pub fn set_style(&self, style: impl Into<Arc<Style>>) {
|
||||||
self.options_mut(|opt| opt.style = style.into());
|
self.options_mut(|opt| opt.style = style.into());
|
||||||
}
|
}
|
||||||
@@ -1365,9 +1375,16 @@ impl Context {
|
|||||||
|
|
||||||
/// Allocate a texture.
|
/// 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`].
|
/// 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:
|
/// // 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(
|
pub fn load_texture(
|
||||||
&self,
|
&self,
|
||||||
name: impl Into<String>,
|
name: impl Into<String>,
|
||||||
@@ -1533,7 +1550,7 @@ impl Context {
|
|||||||
nodes,
|
nodes,
|
||||||
tree: Some(accesskit::Tree::new(root_id)),
|
tree: Some(accesskit::Tree::new(root_id)),
|
||||||
focus: has_focus.then(|| {
|
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())
|
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);
|
let mut size = vec2(w as f32, h as f32);
|
||||||
size *= (max_preview_size.x / size.x).min(1.0);
|
size *= (max_preview_size.x / size.x).min(1.0);
|
||||||
size *= (max_preview_size.y / size.y).min(1.0);
|
size *= (max_preview_size.y / size.y).min(1.0);
|
||||||
ui.image(texture_id, size).on_hover_ui(|ui| {
|
ui.image(SizedTexture::new(texture_id, size))
|
||||||
// show larger on hover
|
.on_hover_ui(|ui| {
|
||||||
let max_size = 0.5 * ui.ctx().screen_rect().size();
|
// show larger on hover
|
||||||
let mut size = vec2(w as f32, h as f32);
|
let max_size = 0.5 * ui.ctx().screen_rect().size();
|
||||||
size *= max_size.x / size.x.max(max_size.x);
|
let mut size = vec2(w as f32, h as f32);
|
||||||
size *= max_size.y / size.y.max(max_size.y);
|
size *= max_size.x / size.x.max(max_size.x);
|
||||||
ui.image(texture_id, size);
|
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!("{w} x {h}"));
|
||||||
ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6));
|
ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6));
|
||||||
@@ -2290,60 +2308,93 @@ impl Context {
|
|||||||
impl Context {
|
impl Context {
|
||||||
/// Associate some static bytes with a `uri`.
|
/// Associate some static bytes with a `uri`.
|
||||||
///
|
///
|
||||||
/// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image.
|
/// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image.
|
||||||
pub fn include_static_bytes(&self, uri: &'static str, bytes: &'static [u8]) {
|
|
||||||
self.read(|ctx| ctx.loaders.include.insert_static(uri, bytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Associate some bytes with a `uri`.
|
|
||||||
///
|
///
|
||||||
/// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image.
|
/// By convention, the `uri` should start with `bytes://`.
|
||||||
pub fn include_bytes(&self, uri: &'static str, bytes: impl Into<Arc<[u8]>>) {
|
/// Following that convention will lead to better error messages.
|
||||||
self.read(|ctx| ctx.loaders.include.insert_shared(uri, bytes));
|
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.
|
/// See [`load`] for more information.
|
||||||
pub fn add_bytes_loader(&self, loader: Arc<dyn load::BytesLoader + Send + Sync + 'static>) {
|
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.
|
/// See [`load`] for more information.
|
||||||
pub fn add_image_loader(&self, loader: Arc<dyn load::ImageLoader + Send + Sync + 'static>) {
|
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.
|
/// See [`load`] for more information.
|
||||||
pub fn add_texture_loader(&self, loader: Arc<dyn load::TextureLoader + Send + Sync + 'static>) {
|
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.
|
/// 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.
|
/// If you attempt to load the image again, it will be reloaded from scratch.
|
||||||
pub fn forget_image(&self, uri: &str) {
|
pub fn forget_image(&self, uri: &str) {
|
||||||
self.write(|ctx| {
|
use load::BytesLoader as _;
|
||||||
use crate::load::BytesLoader as _;
|
|
||||||
|
|
||||||
ctx.loaders.include.forget(uri);
|
crate::profile_function!();
|
||||||
|
|
||||||
for loader in &ctx.loaders.bytes {
|
let loaders = self.loaders();
|
||||||
loader.forget(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
for loader in &ctx.loaders.image {
|
loaders.include.forget(uri);
|
||||||
loader.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 {
|
/// Release all memory and textures related to images used in [`Ui::image`] or [`Image`].
|
||||||
loader.forget(uri);
|
///
|
||||||
}
|
/// 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.
|
/// Try loading the bytes from the given uri using any available bytes loaders.
|
||||||
@@ -2358,21 +2409,27 @@ impl Context {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// This may fail with:
|
/// This may fail with:
|
||||||
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
|
/// - [`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
|
/// [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 {
|
pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult {
|
||||||
self.read(|this| {
|
crate::profile_function!();
|
||||||
for loader in &this.loaders.bytes {
|
|
||||||
match loader.load(self, uri) {
|
|
||||||
Err(load::LoadError::NotSupported) => continue,
|
|
||||||
result => return result,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
/// Try loading the image from the given uri using any available image loaders.
|
||||||
@@ -2386,22 +2443,33 @@ impl Context {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// This may fail with:
|
/// 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::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
|
/// [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 {
|
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
|
||||||
self.read(|this| {
|
crate::profile_function!();
|
||||||
for loader in &this.loaders.image {
|
|
||||||
match loader.load(self, uri, size_hint) {
|
|
||||||
Err(load::LoadError::NotSupported) => continue,
|
|
||||||
result => return result,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(load::LoadError::NotSupported)
|
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.
|
/// Try loading the texture from the given uri using any available texture loaders.
|
||||||
@@ -2416,26 +2484,38 @@ impl Context {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// This may fail with:
|
/// This may fail with:
|
||||||
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
|
/// - [`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
|
/// [not_supported]: crate::load::LoadError::NotSupported
|
||||||
/// [custom]: crate::load::LoadError::Custom
|
/// [custom]: crate::load::LoadError::Loading
|
||||||
pub fn try_load_texture(
|
pub fn try_load_texture(
|
||||||
&self,
|
&self,
|
||||||
uri: &str,
|
uri: &str,
|
||||||
texture_options: TextureOptions,
|
texture_options: TextureOptions,
|
||||||
size_hint: load::SizeHint,
|
size_hint: load::SizeHint,
|
||||||
) -> load::TextureLoadResult {
|
) -> load::TextureLoadResult {
|
||||||
self.read(|this| {
|
crate::profile_function!();
|
||||||
for loader in &this.loaders.texture {
|
|
||||||
match loader.load(self, uri, texture_options, size_hint) {
|
|
||||||
Err(load::LoadError::NotSupported) => continue,
|
|
||||||
result => return result,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(load::LoadError::NotSupported)
|
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)
|
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 {
|
pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize {
|
||||||
self.accesskit_action_requests(id, action).count()
|
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();
|
//! ui.separator();
|
||||||
//!
|
//!
|
||||||
//! # let my_image = egui::TextureId::default();
|
//! # 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.collapsing("Click to see what is hidden!", |ui| {
|
||||||
//! ui.label("Not much, as it turns out");
|
//! 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),
|
//! 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),
|
//! 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
|
//! ``` no_run
|
||||||
//! # fn handle_platform_output(_: egui::PlatformOutput) {}
|
//! # 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
|
//! # 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
|
/// 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"
|
/// Profiling macro for feature "puffin"
|
||||||
macro_rules! profile_function {
|
macro_rules! profile_function {
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
|
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
||||||
puffin::profile_function!($($arg)*);
|
puffin::profile_function!($($arg)*);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -615,12 +668,13 @@ mod profiling_scopes {
|
|||||||
/// Profiling macro for feature "puffin"
|
/// Profiling macro for feature "puffin"
|
||||||
macro_rules! profile_scope {
|
macro_rules! profile_scope {
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
|
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
||||||
puffin::profile_scope!($($arg)*);
|
puffin::profile_scope!($($arg)*);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub(crate) use profile_scope;
|
pub(crate) use profile_scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
pub(crate) use profiling_scopes::*;
|
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/),
|
//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/)
|
||||||
//! which contains reasonable default implementations of these traits. You can get started quickly
|
//! will get you up and running quickly with its reasonable default implementations of the traits described below.
|
||||||
//! using [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
|
//!
|
||||||
|
//! 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
|
//! ## Loading process
|
||||||
//!
|
//!
|
||||||
@@ -14,13 +18,13 @@
|
|||||||
//! The different kinds of loaders represent different layers in the loading process:
|
//! The different kinds of loaders represent different layers in the loading process:
|
||||||
//!
|
//!
|
||||||
//! ```text,ignore
|
//! ```text,ignore
|
||||||
//! ui.image2("file://image.png")
|
//! ui.image("file://image.png")
|
||||||
//! └► ctx.try_load_texture("file://image.png", ...)
|
//! └► Context::try_load_texture
|
||||||
//! └► TextureLoader::load("file://image.png", ...)
|
//! └► TextureLoader::load
|
||||||
//! └► ctx.try_load_image("file://image.png", ...)
|
//! └► Context::try_load_image
|
||||||
//! └► ImageLoader::load("file://image.png", ...)
|
//! └► ImageLoader::load
|
||||||
//! └► ctx.try_load_bytes("file://image.png", ...)
|
//! └► Context::try_load_bytes
|
||||||
//! └► BytesLoader::load("file://image.png", ...)
|
//! └► BytesLoader::load
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! As each layer attempts to load the URI, it first asks the layer below it
|
//! 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
|
//! For example, a loader may determine that it doesn't support loading a specific URI
|
||||||
//! if the protocol does not match what it expects.
|
//! if the protocol does not match what it expects.
|
||||||
|
|
||||||
|
mod bytes_loader;
|
||||||
|
mod texture_loader;
|
||||||
|
|
||||||
use crate::Context;
|
use crate::Context;
|
||||||
use ahash::HashMap;
|
use ahash::HashMap;
|
||||||
use epaint::mutex::Mutex;
|
use epaint::mutex::Mutex;
|
||||||
|
use epaint::util::FloatOrd;
|
||||||
|
use epaint::util::OrderedFloat;
|
||||||
use epaint::TextureHandle;
|
use epaint::TextureHandle;
|
||||||
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
|
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt::Debug;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::{error::Error as StdError, fmt::Display, sync::Arc};
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum LoadError {
|
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,
|
NotSupported,
|
||||||
|
|
||||||
/// A custom error message (e.g. "File not found: foo.png").
|
/// Programmer error: Failed to find the bytes for this image because
|
||||||
Custom(String),
|
/// 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 {
|
impl Display for LoadError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
LoadError::NotSupported => f.write_str("not supported"),
|
Self::NoImageLoaders => f.write_str(
|
||||||
LoadError::Custom(message) => f.write_str(message),
|
"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.
|
/// All variants will preserve the original aspect ratio.
|
||||||
///
|
///
|
||||||
/// Similar to `usvg::FitTo`.
|
/// 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 {
|
pub enum SizeHint {
|
||||||
/// Keep original size.
|
/// Scale original size by some factor.
|
||||||
#[default]
|
Scale(OrderedFloat<f32>),
|
||||||
Original,
|
|
||||||
|
|
||||||
/// Scale to width.
|
/// Scale to width.
|
||||||
Width(u32),
|
Width(u32),
|
||||||
@@ -101,22 +141,36 @@ pub enum SizeHint {
|
|||||||
Size(u32, u32),
|
Size(u32, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for SizeHint {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Scale(1.0.ord())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Vec2> for SizeHint {
|
impl From<Vec2> for SizeHint {
|
||||||
fn from(value: Vec2) -> Self {
|
fn from(value: Vec2) -> Self {
|
||||||
Self::Size(value.x.round() as u32, value.y.round() as u32)
|
Self::Size(value.x.round() as u32, value.y.round() as u32)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: API for querying bytes caches in each loader
|
/// Represents a byte buffer.
|
||||||
|
///
|
||||||
pub type Size = [usize; 2];
|
/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`.
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Bytes {
|
pub enum Bytes {
|
||||||
Static(&'static [u8]),
|
Static(&'static [u8]),
|
||||||
Shared(Arc<[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 {
|
impl From<&'static [u8]> for Bytes {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(value: &'static [u8]) -> Self {
|
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 {
|
impl From<Arc<[u8]>> for Bytes {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(value: Arc<[u8]>) -> Self {
|
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 {
|
impl AsRef<[u8]> for Bytes {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn as_ref(&self) -> &[u8] {
|
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)]
|
#[derive(Clone)]
|
||||||
pub enum BytesPoll {
|
pub enum BytesPoll {
|
||||||
/// Bytes are being loaded.
|
/// Bytes are being loaded.
|
||||||
Pending {
|
Pending {
|
||||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
/// 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.
|
/// Bytes are loaded.
|
||||||
Ready {
|
Ready {
|
||||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
/// 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`.
|
/// File contents, e.g. the contents of a `.png`.
|
||||||
bytes: Bytes,
|
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>;
|
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 {
|
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.
|
/// Try loading the bytes from the given uri.
|
||||||
///
|
///
|
||||||
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
||||||
@@ -182,7 +283,7 @@ pub trait BytesLoader {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// This may fail with:
|
/// This may fail with:
|
||||||
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
|
/// - [`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;
|
fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult;
|
||||||
|
|
||||||
/// Forget the given `uri`.
|
/// Forget the given `uri`.
|
||||||
@@ -191,6 +292,12 @@ pub trait BytesLoader {
|
|||||||
/// so that it may be fully reloaded.
|
/// so that it may be fully reloaded.
|
||||||
fn forget(&self, uri: &str);
|
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,
|
/// Implementations may use this to perform work at the end of a frame,
|
||||||
/// such as evicting unused entries from a cache.
|
/// such as evicting unused entries from a cache.
|
||||||
fn end_frame(&self, frame_index: usize) {
|
fn end_frame(&self, frame_index: usize) {
|
||||||
@@ -201,12 +308,17 @@ pub trait BytesLoader {
|
|||||||
fn byte_size(&self) -> usize;
|
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)]
|
#[derive(Clone)]
|
||||||
pub enum ImagePoll {
|
pub enum ImagePoll {
|
||||||
/// Image is loading.
|
/// Image is loading.
|
||||||
Pending {
|
Pending {
|
||||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
/// 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.
|
/// Image is loaded.
|
||||||
@@ -215,7 +327,18 @@ pub enum ImagePoll {
|
|||||||
|
|
||||||
pub type ImageLoadResult = Result<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 {
|
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.
|
/// Try loading the image from the given uri.
|
||||||
///
|
///
|
||||||
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
||||||
@@ -227,7 +350,7 @@ pub trait ImageLoader {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// This may fail with:
|
/// This may fail with:
|
||||||
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
|
/// - [`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;
|
fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult;
|
||||||
|
|
||||||
/// Forget the given `uri`.
|
/// Forget the given `uri`.
|
||||||
@@ -236,6 +359,12 @@ pub trait ImageLoader {
|
|||||||
/// so that it may be fully reloaded.
|
/// so that it may be fully reloaded.
|
||||||
fn forget(&self, uri: &str);
|
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,
|
/// Implementations may use this to perform work at the end of a frame,
|
||||||
/// such as evicting unused entries from a cache.
|
/// such as evicting unused entries from a cache.
|
||||||
fn end_frame(&self, frame_index: usize) {
|
fn end_frame(&self, frame_index: usize) {
|
||||||
@@ -247,36 +376,91 @@ pub trait ImageLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A texture with a known size.
|
/// A texture with a known size.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct SizedTexture {
|
pub struct SizedTexture {
|
||||||
pub id: TextureId,
|
pub id: TextureId,
|
||||||
pub size: Size,
|
pub size: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SizedTexture {
|
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 {
|
pub fn from_handle(handle: &TextureHandle) -> Self {
|
||||||
|
let size = handle.size();
|
||||||
Self {
|
Self {
|
||||||
id: handle.id(),
|
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 {
|
pub enum TexturePoll {
|
||||||
/// Texture is loading.
|
/// Texture is loading.
|
||||||
Pending {
|
Pending {
|
||||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
/// 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.
|
/// Texture is loaded.
|
||||||
Ready { texture: SizedTexture },
|
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>;
|
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 {
|
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.
|
/// Try loading the texture from the given uri.
|
||||||
///
|
///
|
||||||
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
||||||
@@ -288,7 +472,7 @@ pub trait TextureLoader {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
/// This may fail with:
|
/// This may fail with:
|
||||||
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
|
/// - [`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(
|
fn load(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
@@ -303,6 +487,12 @@ pub trait TextureLoader {
|
|||||||
/// so that it may be fully reloaded.
|
/// so that it may be fully reloaded.
|
||||||
fn forget(&self, uri: &str);
|
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,
|
/// Implementations may use this to perform work at the end of a frame,
|
||||||
/// such as evicting unused entries from a cache.
|
/// such as evicting unused entries from a cache.
|
||||||
fn end_frame(&self, frame_index: usize) {
|
fn end_frame(&self, frame_index: usize) {
|
||||||
@@ -313,104 +503,27 @@ pub trait TextureLoader {
|
|||||||
fn byte_size(&self) -> usize;
|
fn byte_size(&self) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
type BytesLoaderImpl = Arc<dyn BytesLoader + Send + Sync + 'static>;
|
||||||
pub(crate) struct DefaultBytesLoader {
|
type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
|
||||||
cache: Mutex<HashMap<&'static str, Bytes>>,
|
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
|
||||||
}
|
|
||||||
|
|
||||||
impl DefaultBytesLoader {
|
#[derive(Clone)]
|
||||||
pub(crate) fn insert_static(&self, uri: &'static str, bytes: &'static [u8]) {
|
/// The loaders of bytes, images, and textures.
|
||||||
self.cache
|
pub struct Loaders {
|
||||||
.lock()
|
|
||||||
.entry(uri)
|
|
||||||
.or_insert_with(|| Bytes::Static(bytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn insert_shared(&self, uri: &'static str, bytes: impl Into<Arc<[u8]>>) {
|
|
||||||
self.cache
|
|
||||||
.lock()
|
|
||||||
.entry(uri)
|
|
||||||
.or_insert_with(|| Bytes::Shared(bytes.into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BytesLoader for DefaultBytesLoader {
|
|
||||||
fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
|
|
||||||
match self.cache.lock().get(uri).cloned() {
|
|
||||||
Some(bytes) => Ok(BytesPoll::Ready { size: None, bytes }),
|
|
||||||
None => Err(LoadError::NotSupported),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forget(&self, uri: &str) {
|
|
||||||
let _ = self.cache.lock().remove(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn byte_size(&self) -> usize {
|
|
||||||
self.cache.lock().values().map(|bytes| bytes.len()).sum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct DefaultTextureLoader {
|
|
||||||
cache: Mutex<HashMap<(String, TextureOptions), TextureHandle>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextureLoader for DefaultTextureLoader {
|
|
||||||
fn load(
|
|
||||||
&self,
|
|
||||||
ctx: &Context,
|
|
||||||
uri: &str,
|
|
||||||
texture_options: TextureOptions,
|
|
||||||
size_hint: SizeHint,
|
|
||||||
) -> TextureLoadResult {
|
|
||||||
let mut cache = self.cache.lock();
|
|
||||||
if let Some(handle) = cache.get(&(uri.into(), texture_options)) {
|
|
||||||
let texture = SizedTexture::from_handle(handle);
|
|
||||||
Ok(TexturePoll::Ready { texture })
|
|
||||||
} else {
|
|
||||||
match ctx.try_load_image(uri, size_hint)? {
|
|
||||||
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
|
|
||||||
ImagePoll::Ready { image } => {
|
|
||||||
let handle = ctx.load_texture(uri, image, texture_options);
|
|
||||||
let texture = SizedTexture::from_handle(&handle);
|
|
||||||
cache.insert((uri.into(), texture_options), handle);
|
|
||||||
Ok(TexturePoll::Ready { texture })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forget(&self, uri: &str) {
|
|
||||||
self.cache.lock().retain(|(u, _), _| u != uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end_frame(&self, _: usize) {}
|
|
||||||
|
|
||||||
fn byte_size(&self) -> usize {
|
|
||||||
self.cache
|
|
||||||
.lock()
|
|
||||||
.values()
|
|
||||||
.map(|texture| texture.byte_size())
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Loaders {
|
|
||||||
pub include: Arc<DefaultBytesLoader>,
|
pub include: Arc<DefaultBytesLoader>,
|
||||||
pub bytes: Vec<Arc<dyn BytesLoader + Send + Sync + 'static>>,
|
pub bytes: Mutex<Vec<BytesLoaderImpl>>,
|
||||||
pub image: Vec<Arc<dyn ImageLoader + Send + Sync + 'static>>,
|
pub image: Mutex<Vec<ImageLoaderImpl>>,
|
||||||
pub texture: Vec<Arc<dyn TextureLoader + Send + Sync + 'static>>,
|
pub texture: Mutex<Vec<TextureLoaderImpl>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Loaders {
|
impl Default for Loaders {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let include = Arc::new(DefaultBytesLoader::default());
|
let include = Arc::new(DefaultBytesLoader::default());
|
||||||
Self {
|
Self {
|
||||||
bytes: vec![include.clone()],
|
bytes: Mutex::new(vec![include.clone()]),
|
||||||
image: Vec::new(),
|
image: Mutex::new(Vec::new()),
|
||||||
// By default we only include `DefaultTextureLoader`.
|
// By default we only include `DefaultTextureLoader`.
|
||||||
texture: vec![Arc::new(DefaultTextureLoader::default())],
|
texture: Mutex::new(vec![Arc::new(DefaultTextureLoader::default())]),
|
||||||
include,
|
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 ahash::HashMap;
|
||||||
use epaint::{emath::Rangef, vec2, Vec2};
|
use epaint::{emath::Rangef, vec2, Vec2};
|
||||||
|
|
||||||
@@ -236,7 +238,7 @@ pub(crate) struct Interaction {
|
|||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub(crate) struct Focus {
|
pub(crate) struct Focus {
|
||||||
/// The widget with keyboard focus (i.e. a text input field).
|
/// 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?
|
/// What had keyboard focus previous frame?
|
||||||
id_previous_frame: Option<Id>,
|
id_previous_frame: Option<Id>,
|
||||||
@@ -254,9 +256,6 @@ pub(crate) struct Focus {
|
|||||||
/// The last widget interested in focus.
|
/// The last widget interested in focus.
|
||||||
last_interested: Option<Id>,
|
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
|
/// Set when looking for widget with navigational keys like arrows, tab, shift+tab
|
||||||
focus_direction: FocusDirection,
|
focus_direction: FocusDirection,
|
||||||
|
|
||||||
@@ -264,6 +263,22 @@ pub(crate) struct Focus {
|
|||||||
focus_widgets_cache: IdMap<Rect>,
|
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 {
|
impl Interaction {
|
||||||
/// Are we currently clicking or dragging an egui widget?
|
/// Are we currently clicking or dragging an egui widget?
|
||||||
pub fn is_using_pointer(&self) -> bool {
|
pub fn is_using_pointer(&self) -> bool {
|
||||||
@@ -295,14 +310,15 @@ impl Interaction {
|
|||||||
impl Focus {
|
impl Focus {
|
||||||
/// Which widget currently has keyboard focus?
|
/// Which widget currently has keyboard focus?
|
||||||
pub fn focused(&self) -> Option<Id> {
|
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) {
|
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() {
|
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")]
|
#[cfg(feature = "accesskit")]
|
||||||
{
|
{
|
||||||
@@ -312,37 +328,35 @@ impl Focus {
|
|||||||
self.focus_direction = FocusDirection::None;
|
self.focus_direction = FocusDirection::None;
|
||||||
|
|
||||||
for event in &new_input.events {
|
for event in &new_input.events {
|
||||||
if let crate::Event::Key {
|
if !event_filter.matches(event) {
|
||||||
key,
|
if let crate::Event::Key {
|
||||||
pressed: true,
|
key,
|
||||||
modifiers,
|
pressed: true,
|
||||||
..
|
modifiers,
|
||||||
} = event
|
..
|
||||||
{
|
} = event
|
||||||
if let Some(cardinality) = match key {
|
{
|
||||||
crate::Key::ArrowUp => Some(FocusDirection::Up),
|
if let Some(cardinality) = match key {
|
||||||
crate::Key::ArrowRight => Some(FocusDirection::Right),
|
crate::Key::ArrowUp => Some(FocusDirection::Up),
|
||||||
crate::Key::ArrowDown => Some(FocusDirection::Down),
|
crate::Key::ArrowRight => Some(FocusDirection::Right),
|
||||||
crate::Key::ArrowLeft => Some(FocusDirection::Left),
|
crate::Key::ArrowDown => Some(FocusDirection::Down),
|
||||||
crate::Key::Tab => {
|
crate::Key::ArrowLeft => Some(FocusDirection::Left),
|
||||||
if !self.is_focus_locked {
|
|
||||||
|
crate::Key::Tab => {
|
||||||
if modifiers.shift {
|
if modifiers.shift {
|
||||||
Some(FocusDirection::Previous)
|
Some(FocusDirection::Previous)
|
||||||
} else {
|
} else {
|
||||||
Some(FocusDirection::Next)
|
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>) {
|
pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
|
||||||
if self.focus_direction.is_cardinal() {
|
if self.focus_direction.is_cardinal() {
|
||||||
if let Some(found_widget) = self.find_widget_in_direction(used_ids) {
|
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
|
// 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!
|
// 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")]
|
#[cfg(feature = "accesskit")]
|
||||||
{
|
{
|
||||||
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
|
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.id_requested_by_accesskit = None;
|
||||||
self.give_to_next = false;
|
self.give_to_next = false;
|
||||||
self.reset_focus();
|
self.reset_focus();
|
||||||
@@ -399,23 +413,23 @@ impl Focus {
|
|||||||
.or_insert(Rect::EVERYTHING);
|
.or_insert(Rect::EVERYTHING);
|
||||||
|
|
||||||
if self.give_to_next && !self.had_focus_last_frame(id) {
|
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;
|
self.give_to_next = false;
|
||||||
} else if self.id == Some(id) {
|
} else if self.focused() == Some(id) {
|
||||||
if self.focus_direction == FocusDirection::Next && !self.is_focus_locked {
|
if self.focus_direction == FocusDirection::Next {
|
||||||
self.id = None;
|
self.focused_widget = None;
|
||||||
self.give_to_next = true;
|
self.give_to_next = true;
|
||||||
self.reset_focus();
|
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.id_next_frame = self.last_interested; // frame-delay so gained_focus works
|
||||||
self.reset_focus();
|
self.reset_focus();
|
||||||
}
|
}
|
||||||
} else if self.focus_direction == FocusDirection::Next
|
} else if self.focus_direction == FocusDirection::Next
|
||||||
&& self.id.is_none()
|
&& self.focused_widget.is_none()
|
||||||
&& !self.give_to_next
|
&& !self.give_to_next
|
||||||
{
|
{
|
||||||
// nothing has focus and the user pressed tab - give focus to the first widgets that wants it:
|
// 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();
|
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;
|
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;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -474,7 +488,7 @@ impl Focus {
|
|||||||
let mut best_id = None;
|
let mut best_id = None;
|
||||||
|
|
||||||
for (candidate_id, candidate_rect) in &self.focus_widgets_cache {
|
for (candidate_id, candidate_rect) in &self.focus_widgets_cache {
|
||||||
if Some(*candidate_id) == self.id {
|
if *candidate_id == current_focused.id {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,46 +619,58 @@ impl Memory {
|
|||||||
/// from the window and back.
|
/// from the window and back.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn has_focus(&self, id: Id) -> bool {
|
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?
|
/// Which widget has keyboard focus?
|
||||||
pub fn focus(&self) -> Option<Id> {
|
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.
|
/// 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) {
|
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.
|
/// Set an event filter for a widget.
|
||||||
pub fn has_lock_focus(&self, id: Id) -> bool {
|
///
|
||||||
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
/// You must first give focus to the widget before calling this.
|
||||||
self.interaction.focus.is_focus_locked
|
#[deprecated = "Use set_focus_lock_filter instead"]
|
||||||
} else {
|
pub fn lock_focus(&mut self, id: Id, lock_focus: bool) {
|
||||||
false
|
self.set_focus_lock_filter(
|
||||||
}
|
id,
|
||||||
|
EventFilter {
|
||||||
|
tab: lock_focus,
|
||||||
|
arrows: lock_focus,
|
||||||
|
escape: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Give keyboard focus to a specific widget.
|
/// Give keyboard focus to a specific widget.
|
||||||
/// See also [`crate::Response::request_focus`].
|
/// See also [`crate::Response::request_focus`].
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn request_focus(&mut self, id: Id) {
|
pub fn request_focus(&mut self, id: Id) {
|
||||||
self.interaction.focus.id = Some(id);
|
self.interaction.focus.focused_widget = Some(FocusWidget::new(id));
|
||||||
self.interaction.focus.is_focus_locked = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Surrender keyboard focus for a specific widget.
|
/// Surrender keyboard focus for a specific widget.
|
||||||
/// See also [`crate::Response::surrender_focus`].
|
/// See also [`crate::Response::surrender_focus`].
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn surrender_focus(&mut self, id: Id) {
|
pub fn surrender_focus(&mut self, id: Id) {
|
||||||
if self.interaction.focus.id == Some(id) {
|
if self.interaction.focus.focused() == Some(id) {
|
||||||
self.interaction.focus.id = None;
|
self.interaction.focus.focused_widget = None;
|
||||||
self.interaction.focus.is_focus_locked = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,7 +689,7 @@ impl Memory {
|
|||||||
/// Stop editing of active [`TextEdit`](crate::TextEdit) (if any).
|
/// Stop editing of active [`TextEdit`](crate::TextEdit) (if any).
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn stop_text_input(&mut self) {
|
pub fn stop_text_input(&mut self) {
|
||||||
self.interaction.focus.id = None;
|
self.interaction.focus.focused_widget = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ pub fn menu_button<R>(
|
|||||||
/// Returns `None` if the menu is not open.
|
/// Returns `None` if the menu is not open.
|
||||||
pub fn menu_image_button<R>(
|
pub fn menu_image_button<R>(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
image_button: ImageButton,
|
image_button: ImageButton<'_>,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> InnerResponse<Option<R>> {
|
) -> InnerResponse<Option<R>> {
|
||||||
stationary_menu_image_impl(ui, image_button, Box::new(add_contents))
|
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.
|
/// Responds to primary clicks.
|
||||||
fn stationary_menu_image_impl<'c, R>(
|
fn stationary_menu_image_impl<'c, R>(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
image_button: ImageButton,
|
image_button: ImageButton<'_>,
|
||||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||||
) -> InnerResponse<Option<R>> {
|
) -> InnerResponse<Option<R>> {
|
||||||
let bar_id = ui.id();
|
let bar_id = ui.id();
|
||||||
|
|||||||
@@ -350,6 +350,18 @@ impl Painter {
|
|||||||
/// unless you want to crop or flip the image.
|
/// 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.
|
/// `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) {
|
pub fn image(&self, texture_id: epaint::TextureId, rect: Rect, uv: Rect, tint: Color32) {
|
||||||
self.add(Shape::image(texture_id, rect, uv, tint));
|
self.add(Shape::image(texture_id, rect, uv, tint));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
#![allow(clippy::if_same_then_else)]
|
#![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 epaint::{Rounding, Shadow, Stroke};
|
||||||
use std::collections::BTreeMap;
|
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`].
|
/// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`].
|
||||||
pub slider_trailing_fill: bool,
|
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 {
|
impl Visuals {
|
||||||
@@ -806,6 +818,10 @@ impl Visuals {
|
|||||||
striped: false,
|
striped: false,
|
||||||
|
|
||||||
slider_trailing_fill: false,
|
slider_trailing_fill: false,
|
||||||
|
|
||||||
|
interact_cursor: None,
|
||||||
|
|
||||||
|
image_loading_spinners: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1376,6 +1392,9 @@ impl Visuals {
|
|||||||
striped,
|
striped,
|
||||||
|
|
||||||
slider_trailing_fill,
|
slider_trailing_fill,
|
||||||
|
interact_cursor,
|
||||||
|
|
||||||
|
image_loading_spinners,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
ui.collapsing("Background Colors", |ui| {
|
ui.collapsing("Background Colors", |ui| {
|
||||||
@@ -1441,6 +1460,19 @@ impl Visuals {
|
|||||||
|
|
||||||
ui.checkbox(slider_trailing_fill, "Add trailing color to sliders");
|
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));
|
ui.vertical_centered(|ui| reset_button(ui, self));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1558,56 +1558,34 @@ impl Ui {
|
|||||||
response
|
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`.
|
/// Show an image available at the given `uri`.
|
||||||
///
|
///
|
||||||
/// ⚠ This will do nothing unless you install some image loaders first!
|
/// ⚠ 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.
|
/// The loaders handle caching image data, sampled textures, etc. across frames, so calling this is immediate-mode safe.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # egui::__run_test_ui(|ui| {
|
/// # 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]
|
#[inline]
|
||||||
pub fn image2<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
|
pub fn image<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
|
||||||
Image2::new(source.into()).ui(self)
|
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> {
|
pub fn group<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||||
crate::Frame::group(self.style()).show(self, add_contents)
|
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.
|
/// If called from within a menu this will instead create a button for a sub-menu.
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use egui_extras;
|
/// let img = egui::include_image!("../assets/ferris.png");
|
||||||
///
|
///
|
||||||
/// let img = egui_extras::RetainedImage::from_svg_bytes_with_size(
|
/// ui.menu_image_button(img, |ui| {
|
||||||
/// "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_button("My sub-menu", |ui| {
|
/// ui.menu_button("My sub-menu", |ui| {
|
||||||
/// if ui.button("Close the menu").clicked() {
|
/// if ui.button("Close the menu").clicked() {
|
||||||
/// ui.close_menu();
|
/// ui.close_menu();
|
||||||
@@ -2225,16 +2197,15 @@ impl Ui {
|
|||||||
///
|
///
|
||||||
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
|
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn menu_image_button<R>(
|
pub fn menu_image_button<'a, R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
texture_id: TextureId,
|
image: impl Into<Image<'a>>,
|
||||||
image_size: impl Into<Vec2>,
|
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> InnerResponse<Option<R>> {
|
) -> InnerResponse<Option<R>> {
|
||||||
if let Some(menu_state) = self.menu_state.clone() {
|
if let Some(menu_state) = self.menu_state.clone() {
|
||||||
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
||||||
} else {
|
} 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)
|
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(
|
fn into_text_job(
|
||||||
self,
|
self,
|
||||||
style: &Style,
|
style: &Style,
|
||||||
fallback_font: FontSelection,
|
fallback_font: FontSelection,
|
||||||
default_valign: Align,
|
default_valign: Align,
|
||||||
) -> WidgetTextJob {
|
) -> 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 text_color = self.get_text_color(&style.visuals);
|
||||||
|
|
||||||
let Self {
|
let Self {
|
||||||
@@ -295,7 +349,6 @@ impl RichText {
|
|||||||
raised,
|
raised,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let job_has_color = text_color.is_some();
|
|
||||||
let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
|
let line_color = text_color.unwrap_or_else(|| style.visuals.text_color());
|
||||||
let text_color = text_color.unwrap_or(crate::Color32::TEMPORARY_COLOR);
|
let text_color = text_color.unwrap_or(crate::Color32::TEMPORARY_COLOR);
|
||||||
|
|
||||||
@@ -336,20 +389,20 @@ impl RichText {
|
|||||||
default_valign
|
default_valign
|
||||||
};
|
};
|
||||||
|
|
||||||
let text_format = crate::text::TextFormat {
|
(
|
||||||
font_id,
|
text,
|
||||||
extra_letter_spacing,
|
crate::text::TextFormat {
|
||||||
line_height,
|
font_id,
|
||||||
color: text_color,
|
extra_letter_spacing,
|
||||||
background: background_color,
|
line_height,
|
||||||
italics,
|
color: text_color,
|
||||||
underline,
|
background: background_color,
|
||||||
strikethrough,
|
italics,
|
||||||
valign,
|
underline,
|
||||||
};
|
strikethrough,
|
||||||
|
valign,
|
||||||
let job = LayoutJob::single_section(text, text_format);
|
},
|
||||||
WidgetTextJob { job, job_has_color }
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_text_color(&self, visuals: &Visuals) -> Option<Color32> {
|
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);`"]
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
pub struct Button {
|
pub struct Button<'a> {
|
||||||
text: WidgetText,
|
image: Option<Image<'a>>,
|
||||||
|
text: Option<WidgetText>,
|
||||||
shortcut_text: WidgetText,
|
shortcut_text: WidgetText,
|
||||||
wrap: Option<bool>,
|
wrap: Option<bool>,
|
||||||
|
|
||||||
@@ -32,13 +33,30 @@ pub struct Button {
|
|||||||
frame: Option<bool>,
|
frame: Option<bool>,
|
||||||
min_size: Vec2,
|
min_size: Vec2,
|
||||||
rounding: Option<Rounding>,
|
rounding: Option<Rounding>,
|
||||||
image: Option<widgets::Image>,
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Button {
|
impl<'a> Button<'a> {
|
||||||
pub fn new(text: impl Into<WidgetText>) -> Self {
|
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 {
|
Self {
|
||||||
text: text.into(),
|
text,
|
||||||
|
image,
|
||||||
shortcut_text: Default::default(),
|
shortcut_text: Default::default(),
|
||||||
wrap: None,
|
wrap: None,
|
||||||
fill: None,
|
fill: None,
|
||||||
@@ -48,20 +66,7 @@ impl Button {
|
|||||||
frame: None,
|
frame: None,
|
||||||
min_size: Vec2::ZERO,
|
min_size: Vec2::ZERO,
|
||||||
rounding: None,
|
rounding: None,
|
||||||
image: None,
|
selected: false,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +101,9 @@ impl Button {
|
|||||||
|
|
||||||
/// Make this a small button, suitable for embedding into text.
|
/// Make this a small button, suitable for embedding into text.
|
||||||
pub fn small(mut self) -> Self {
|
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.small = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -135,12 +142,19 @@ impl Button {
|
|||||||
self.shortcut_text = shortcut_text.into();
|
self.shortcut_text = shortcut_text.into();
|
||||||
self
|
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 {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
let Button {
|
let Button {
|
||||||
text,
|
text,
|
||||||
|
image,
|
||||||
shortcut_text,
|
shortcut_text,
|
||||||
wrap,
|
wrap,
|
||||||
fill,
|
fill,
|
||||||
@@ -150,32 +164,58 @@ impl Widget for Button {
|
|||||||
frame,
|
frame,
|
||||||
min_size,
|
min_size,
|
||||||
rounding,
|
rounding,
|
||||||
image,
|
selected,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
|
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 {
|
if small {
|
||||||
button_padding.y = 0.0;
|
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;
|
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
|
||||||
if let Some(image) = image {
|
if image.is_some() {
|
||||||
text_wrap_width -= image.size().x + ui.spacing().icon_spacing;
|
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
|
||||||
}
|
}
|
||||||
if !shortcut_text.is_empty() {
|
if !shortcut_text.is_empty() {
|
||||||
text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap).
|
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())
|
let shortcut_text = (!shortcut_text.is_empty())
|
||||||
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
|
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
|
||||||
|
|
||||||
let mut desired_size = text.size();
|
let mut desired_size = Vec2::ZERO;
|
||||||
if let Some(image) = image {
|
if image.is_some() {
|
||||||
desired_size.x += image.size().x + ui.spacing().icon_spacing;
|
desired_size.x += image_size.x;
|
||||||
desired_size.y = desired_size.y.max(image.size().y);
|
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 {
|
if let Some(shortcut_text) = &shortcut_text {
|
||||||
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
|
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);
|
desired_size = desired_size.at_least(min_size);
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
let (rect, mut response) = ui.allocate_at_least(desired_size, sense);
|
||||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text()));
|
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) {
|
if ui.is_rect_visible(rect) {
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
|
|
||||||
if frame {
|
let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected {
|
||||||
let fill = fill.unwrap_or(visuals.weak_bg_fill);
|
let selection = ui.visuals().selection;
|
||||||
let stroke = stroke.unwrap_or(visuals.bg_stroke);
|
(
|
||||||
let rounding = rounding.unwrap_or(visuals.rounding);
|
Vec2::ZERO,
|
||||||
ui.painter()
|
Rounding::ZERO,
|
||||||
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
|
selection.bg_fill,
|
||||||
}
|
selection.stroke,
|
||||||
|
)
|
||||||
let text_pos = if let Some(image) = image {
|
} else if frame {
|
||||||
let icon_spacing = ui.spacing().icon_spacing;
|
let expansion = Vec2::splat(visuals.expansion);
|
||||||
pos2(
|
(
|
||||||
rect.min.x + button_padding.x + image.size().x + icon_spacing,
|
expansion,
|
||||||
rect.center().y - 0.5 * text.size().y,
|
visuals.rounding,
|
||||||
|
visuals.weak_bg_fill,
|
||||||
|
visuals.bg_stroke,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ui.layout()
|
Default::default()
|
||||||
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
|
|
||||||
.min
|
|
||||||
};
|
};
|
||||||
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 {
|
if let Some(shortcut_text) = shortcut_text {
|
||||||
let shortcut_text_pos = pos2(
|
let shortcut_text_pos = pos2(
|
||||||
@@ -225,16 +315,11 @@ impl Widget for Button {
|
|||||||
ui.visuals().weak_text_color(),
|
ui.visuals().weak_text_color(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(image) = image {
|
if let Some(cursor) = ui.visuals().interact_cursor {
|
||||||
let image_rect = Rect::from_min_size(
|
if response.hovered {
|
||||||
pos2(
|
ui.ctx().set_cursor_icon(cursor);
|
||||||
rect.min.x + button_padding.x,
|
|
||||||
rect.center().y - 0.5 - (image.size().y / 2.0),
|
|
||||||
),
|
|
||||||
image.size(),
|
|
||||||
);
|
|
||||||
image.paint_at(ui, image_rect);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,17 +547,17 @@ impl Widget for RadioButton {
|
|||||||
/// A clickable image within a frame.
|
/// A clickable image within a frame.
|
||||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ImageButton {
|
pub struct ImageButton<'a> {
|
||||||
image: widgets::Image,
|
image: Image<'a>,
|
||||||
sense: Sense,
|
sense: Sense,
|
||||||
frame: bool,
|
frame: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageButton {
|
impl<'a> ImageButton<'a> {
|
||||||
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
|
pub fn new(image: impl Into<Image<'a>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
image: widgets::Image::new(texture_id, size),
|
image: image.into(),
|
||||||
sense: Sense::click(),
|
sense: Sense::click(),
|
||||||
frame: true,
|
frame: true,
|
||||||
selected: false,
|
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 {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
let Self {
|
let padding = if self.frame {
|
||||||
image,
|
|
||||||
sense,
|
|
||||||
frame,
|
|
||||||
selected,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let padding = if frame {
|
|
||||||
// so we can see that it is a button:
|
// so we can see that it is a button:
|
||||||
Vec2::splat(ui.spacing().button_padding.x)
|
Vec2::splat(ui.spacing().button_padding.x)
|
||||||
} else {
|
} else {
|
||||||
Vec2::ZERO
|
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));
|
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
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;
|
let selection = ui.visuals().selection;
|
||||||
(
|
(
|
||||||
Vec2::ZERO,
|
Vec2::ZERO,
|
||||||
@@ -539,7 +625,7 @@ impl Widget for ImageButton {
|
|||||||
selection.bg_fill,
|
selection.bg_fill,
|
||||||
selection.stroke,
|
selection.stroke,
|
||||||
)
|
)
|
||||||
} else if frame {
|
} else if self.frame {
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
let expansion = Vec2::splat(visuals.expansion);
|
let expansion = Vec2::splat(visuals.expansion);
|
||||||
(
|
(
|
||||||
@@ -552,23 +638,25 @@ impl Widget for ImageButton {
|
|||||||
Default::default()
|
Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let image = image.rounding(rounding); // apply rounding to the image
|
|
||||||
|
|
||||||
// Draw frame background (for transparent images):
|
// Draw frame background (for transparent images):
|
||||||
ui.painter()
|
ui.painter()
|
||||||
.rect_filled(rect.expand2(expansion), rounding, fill);
|
.rect_filled(rect.expand2(expansion), rounding, fill);
|
||||||
|
|
||||||
let image_rect = ui
|
let image_rect = ui
|
||||||
.layout()
|
.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
|
// 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:
|
// Draw frame outline:
|
||||||
ui.painter()
|
ui.painter()
|
||||||
.rect_stroke(rect.expand2(expansion), rounding, stroke);
|
.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 button::*;
|
||||||
pub use drag_value::DragValue;
|
pub use drag_value::DragValue;
|
||||||
pub use hyperlink::*;
|
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 label::*;
|
||||||
pub use progress_bar::ProgressBar;
|
pub use progress_bar::ProgressBar;
|
||||||
pub use selected_label::SelectableLabel;
|
pub use selected_label::SelectableLabel;
|
||||||
|
|||||||
@@ -570,6 +570,16 @@ impl<'a> Slider<'a> {
|
|||||||
let mut increment = 0usize;
|
let mut increment = 0usize;
|
||||||
|
|
||||||
if response.has_focus() {
|
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 {
|
let (dec_key, inc_key) = match self.orientation {
|
||||||
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
|
SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight),
|
||||||
// Note that this is for moving the slider position,
|
// 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;
|
let kb_step = increment as f32 - decrement as f32;
|
||||||
|
|
||||||
if kb_step != 0.0 {
|
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_value = self.get_value();
|
||||||
let prev_position = self.position_from_value(prev_value, position_range);
|
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 {
|
let new_value = match self.step {
|
||||||
Some(step) => prev_value + (kb_step as f64 * step),
|
Some(step) => prev_value + (kb_step as f64 * step),
|
||||||
None if self.smart_aim => {
|
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(
|
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),
|
||||||
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};
|
use crate::{Response, Sense, Ui, Widget};
|
||||||
|
|
||||||
@@ -31,21 +31,15 @@ impl Spinner {
|
|||||||
self.color = Some(color.into());
|
self.color = Some(color.into());
|
||||||
self
|
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) {
|
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 radius = (rect.height() / 2.0) - 2.0;
|
||||||
let n_points = 20;
|
let n_points = 20;
|
||||||
let time = ui.input(|i| i.time);
|
let time = ui.input(|i| i.time);
|
||||||
@@ -61,6 +55,16 @@ impl Widget for Spinner {
|
|||||||
ui.painter()
|
ui.painter()
|
||||||
.add(Shape::line(points, Stroke::new(3.0, color)));
|
.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
|
response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ pub struct TextEdit<'t> {
|
|||||||
interactive: bool,
|
interactive: bool,
|
||||||
desired_width: Option<f32>,
|
desired_width: Option<f32>,
|
||||||
desired_height_rows: usize,
|
desired_height_rows: usize,
|
||||||
lock_focus: bool,
|
event_filter: EventFilter,
|
||||||
cursor_at_end: bool,
|
cursor_at_end: bool,
|
||||||
min_size: Vec2,
|
min_size: Vec2,
|
||||||
align: Align2,
|
align: Align2,
|
||||||
@@ -115,7 +115,11 @@ impl<'t> TextEdit<'t> {
|
|||||||
interactive: true,
|
interactive: true,
|
||||||
desired_width: None,
|
desired_width: None,
|
||||||
desired_height_rows: 4,
|
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,
|
cursor_at_end: true,
|
||||||
min_size: Vec2::ZERO,
|
min_size: Vec2::ZERO,
|
||||||
align: Align2::LEFT_TOP,
|
align: Align2::LEFT_TOP,
|
||||||
@@ -127,7 +131,7 @@ impl<'t> TextEdit<'t> {
|
|||||||
/// Build a [`TextEdit`] focused on code editing.
|
/// Build a [`TextEdit`] focused on code editing.
|
||||||
/// By default it comes with:
|
/// By default it comes with:
|
||||||
/// - monospaced font
|
/// - monospaced font
|
||||||
/// - focus lock
|
/// - focus lock (tab will insert a tab character instead of moving focus)
|
||||||
pub fn code_editor(self) -> Self {
|
pub fn code_editor(self) -> Self {
|
||||||
self.font(TextStyle::Monospace).lock_focus(true)
|
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
|
/// When `true`, the widget will keep the focus and pressing TAB
|
||||||
/// will insert the `'\t'` character.
|
/// will insert the `'\t'` character.
|
||||||
pub fn lock_focus(mut self, b: bool) -> Self {
|
pub fn lock_focus(mut self, tab_will_indent: bool) -> Self {
|
||||||
self.lock_focus = b;
|
self.event_filter.tab = tab_will_indent;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +356,9 @@ impl<'t> TextEdit<'t> {
|
|||||||
let margin = self.margin;
|
let margin = self.margin;
|
||||||
let max_rect = ui.available_rect_before_wrap().shrink2(margin);
|
let max_rect = ui.available_rect_before_wrap().shrink2(margin);
|
||||||
let mut content_ui = ui.child_ui(max_rect, *ui.layout());
|
let mut content_ui = ui.child_ui(max_rect, *ui.layout());
|
||||||
|
|
||||||
let mut output = self.show_content(&mut content_ui);
|
let mut output = self.show_content(&mut content_ui);
|
||||||
|
|
||||||
let id = output.response.id;
|
let id = output.response.id;
|
||||||
let frame_rect = output.response.rect.expand2(margin);
|
let frame_rect = output.response.rect.expand2(margin);
|
||||||
ui.allocate_space(frame_rect.size());
|
ui.allocate_space(frame_rect.size());
|
||||||
@@ -413,7 +419,7 @@ impl<'t> TextEdit<'t> {
|
|||||||
interactive,
|
interactive,
|
||||||
desired_width,
|
desired_width,
|
||||||
desired_height_rows,
|
desired_height_rows,
|
||||||
lock_focus,
|
event_filter,
|
||||||
cursor_at_end,
|
cursor_at_end,
|
||||||
min_size,
|
min_size,
|
||||||
align,
|
align,
|
||||||
@@ -569,7 +575,7 @@ impl<'t> TextEdit<'t> {
|
|||||||
let mut cursor_range = None;
|
let mut cursor_range = None;
|
||||||
let prev_cursor_range = state.cursor_range(&galley);
|
let prev_cursor_range = state.cursor_range(&galley);
|
||||||
if interactive && ui.memory(|mem| mem.has_focus(id)) {
|
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 {
|
let default_cursor_range = if cursor_at_end {
|
||||||
CursorRange::one(galley.end())
|
CursorRange::one(galley.end())
|
||||||
@@ -589,6 +595,7 @@ impl<'t> TextEdit<'t> {
|
|||||||
password,
|
password,
|
||||||
default_cursor_range,
|
default_cursor_range,
|
||||||
char_limit,
|
char_limit,
|
||||||
|
event_filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
@@ -880,6 +887,7 @@ fn events(
|
|||||||
password: bool,
|
password: bool,
|
||||||
default_cursor_range: CursorRange,
|
default_cursor_range: CursorRange,
|
||||||
char_limit: usize,
|
char_limit: usize,
|
||||||
|
event_filter: EventFilter,
|
||||||
) -> (bool, CursorRange) {
|
) -> (bool, CursorRange) {
|
||||||
let mut cursor_range = state.cursor_range(galley).unwrap_or(default_cursor_range);
|
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 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 {
|
for event in &events {
|
||||||
let did_mutate_text = match event {
|
let did_mutate_text = match event {
|
||||||
Event::Copy => {
|
Event::Copy => {
|
||||||
@@ -946,19 +954,15 @@ fn events(
|
|||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
modifiers,
|
||||||
..
|
..
|
||||||
} => {
|
} if multiline => {
|
||||||
if multiline && ui.memory(|mem| mem.has_lock_focus(id)) {
|
let mut ccursor = delete_selected(text, &cursor_range);
|
||||||
let mut ccursor = delete_selected(text, &cursor_range);
|
if modifiers.shift {
|
||||||
if modifiers.shift {
|
// TODO(emilk): support removing indentation over a selection?
|
||||||
// TODO(emilk): support removing indentation over a selection?
|
decrease_indentation(&mut ccursor, text);
|
||||||
decrease_indentation(&mut ccursor, text);
|
|
||||||
} else {
|
|
||||||
insert_text(&mut ccursor, text, "\t", char_limit);
|
|
||||||
}
|
|
||||||
Some(CCursorRange::one(ccursor))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
insert_text(&mut ccursor, text, "\t", char_limit);
|
||||||
}
|
}
|
||||||
|
Some(CCursorRange::one(ccursor))
|
||||||
}
|
}
|
||||||
Event::Key {
|
Event::Key {
|
||||||
key: Key::Enter,
|
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
|
/// Trait constraining what types [`crate::TextEdit`] may use as
|
||||||
/// an underlying buffer.
|
/// 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`!
|
/// Immutable view of a `&str`!
|
||||||
impl<'a> TextBuffer for &'a str {
|
impl<'a> TextBuffer for &'a str {
|
||||||
fn is_mutable(&self) -> bool {
|
fn is_mutable(&self) -> bool {
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
default = ["glow", "persistence"]
|
default = ["glow", "persistence"]
|
||||||
|
|
||||||
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
|
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
|
||||||
|
image_viewer = ["image", "egui_extras/all_loaders", "rfd"]
|
||||||
persistence = ["eframe/persistence", "egui/persistence", "serde"]
|
persistence = ["eframe/persistence", "egui/persistence", "serde"]
|
||||||
web_screen_reader = ["eframe/web_screen_reader"] # experimental
|
web_screen_reader = ["eframe/web_screen_reader"] # experimental
|
||||||
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
|
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
|
||||||
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
|
syntect = ["egui_demo_lib/syntect"]
|
||||||
|
|
||||||
glow = ["eframe/glow"]
|
glow = ["eframe/glow"]
|
||||||
wgpu = ["eframe/wgpu", "bytemuck"]
|
wgpu = ["eframe/wgpu", "bytemuck"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4", default-features = false, features = [
|
chrono = { version = "0.4", default-features = false, features = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -36,6 +36,7 @@ chrono = { version = "0.4", default-features = false, features = [
|
|||||||
eframe = { version = "0.22.0", path = "../eframe", default-features = false }
|
eframe = { version = "0.22.0", path = "../eframe", default-features = false }
|
||||||
egui = { version = "0.22.0", path = "../egui", features = [
|
egui = { version = "0.22.0", path = "../egui", features = [
|
||||||
"extra_debug_asserts",
|
"extra_debug_asserts",
|
||||||
|
"log",
|
||||||
] }
|
] }
|
||||||
egui_demo_lib = { version = "0.22.0", path = "../egui_demo_lib", features = [
|
egui_demo_lib = { version = "0.22.0", path = "../egui_demo_lib", features = [
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -45,9 +46,10 @@ log = { version = "0.4", features = ["std"] }
|
|||||||
# Optional dependencies:
|
# Optional dependencies:
|
||||||
|
|
||||||
bytemuck = { version = "1.7.1", optional = true }
|
bytemuck = { version = "1.7.1", optional = true }
|
||||||
egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras", features = [
|
egui_extras = { version = "0.22.0", path = "../egui_extras", features = [
|
||||||
"log",
|
"image",
|
||||||
] }
|
] }
|
||||||
|
rfd = { version = "0.11", optional = true }
|
||||||
|
|
||||||
# feature "http":
|
# feature "http":
|
||||||
ehttp = { version = "0.3.0", optional = true }
|
ehttp = { version = "0.3.0", optional = true }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use egui_extras::RetainedImage;
|
use egui::Image;
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
|
|
||||||
struct Resource {
|
struct Resource {
|
||||||
@@ -8,7 +8,7 @@ struct Resource {
|
|||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
|
|
||||||
/// If set, the response was an image.
|
/// 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").
|
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
|
||||||
colored_text: Option<ColoredText>,
|
colored_text: Option<ColoredText>,
|
||||||
@@ -17,21 +17,27 @@ struct Resource {
|
|||||||
impl Resource {
|
impl Resource {
|
||||||
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
|
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
|
||||||
let content_type = response.content_type().unwrap_or_default();
|
let content_type = response.content_type().unwrap_or_default();
|
||||||
let image = if content_type.starts_with("image/") {
|
if content_type.starts_with("image/") {
|
||||||
RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
|
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 {
|
} 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();
|
Self {
|
||||||
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
|
response,
|
||||||
let text = text.map(|text| text.to_owned());
|
text,
|
||||||
|
colored_text,
|
||||||
Self {
|
image: None,
|
||||||
response,
|
}
|
||||||
text,
|
|
||||||
image,
|
|
||||||
colored_text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +69,7 @@ impl eframe::App for HttpApp {
|
|||||||
});
|
});
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
let prev_url = self.url.clone();
|
||||||
let trigger_fetch = ui_url(ui, frame, &mut self.url);
|
let trigger_fetch = ui_url(ui, frame, &mut self.url);
|
||||||
|
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
@@ -77,6 +84,7 @@ impl eframe::App for HttpApp {
|
|||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
let request = ehttp::Request::get(&self.url);
|
let request = ehttp::Request::get(&self.url);
|
||||||
ehttp::fetch(request, move |response| {
|
ehttp::fetch(request, move |response| {
|
||||||
|
ctx.forget_image(&prev_url);
|
||||||
ctx.request_repaint(); // wake up UI thread
|
ctx.request_repaint(); // wake up UI thread
|
||||||
let resource = response.map(|response| Resource::from_response(&ctx, response));
|
let resource = response.map(|response| Resource::from_response(&ctx, response));
|
||||||
sender.send(resource);
|
sender.send(resource);
|
||||||
@@ -193,9 +201,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(image) = image {
|
if let Some(image) = image {
|
||||||
let mut size = image.size_vec2();
|
ui.add(image.clone());
|
||||||
size *= (ui.available_width() / size.x).min(1.0);
|
|
||||||
image.show_size(ui, size);
|
|
||||||
} else if let Some(colored_text) = colored_text {
|
} else if let Some(colored_text) = colored_text {
|
||||||
colored_text.ui(ui);
|
colored_text.ui(ui);
|
||||||
} else if let Some(text) = &text {
|
} else if let Some(text) = &text {
|
||||||
@@ -217,25 +223,19 @@ fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
|
|||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Syntax highlighting:
|
// Syntax highlighting:
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
fn syntax_highlighting(
|
fn syntax_highlighting(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
response: &ehttp::Response,
|
response: &ehttp::Response,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> Option<ColoredText> {
|
) -> Option<ColoredText> {
|
||||||
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
|
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
|
||||||
let extension = extension_and_rest.get(0)?;
|
let extension = extension_and_rest.first()?;
|
||||||
let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style());
|
let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(&ctx.style());
|
||||||
Some(ColoredText(crate::syntax_highlighting::highlight(
|
Some(ColoredText(egui_extras::syntax_highlighting::highlight(
|
||||||
ctx, &theme, text, extension,
|
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);
|
struct ColoredText(egui::text::LayoutJob);
|
||||||
|
|
||||||
impl ColoredText {
|
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")]
|
#[cfg(feature = "http")]
|
||||||
mod http_app;
|
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")))]
|
#[cfg(all(feature = "glow", not(feature = "wgpu")))]
|
||||||
pub use custom3d_glow::Custom3d;
|
pub use custom3d_glow::Custom3d;
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ enum Anchor {
|
|||||||
EasyMarkEditor,
|
EasyMarkEditor,
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
Http,
|
Http,
|
||||||
|
#[cfg(feature = "image_viewer")]
|
||||||
|
ImageViewer,
|
||||||
Clock,
|
Clock,
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
Custom3d,
|
Custom3d,
|
||||||
@@ -142,6 +144,8 @@ pub struct State {
|
|||||||
easy_mark_editor: EasyMarkApp,
|
easy_mark_editor: EasyMarkApp,
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
http: crate::apps::HttpApp,
|
http: crate::apps::HttpApp,
|
||||||
|
#[cfg(feature = "image_viewer")]
|
||||||
|
image_viewer: crate::apps::ImageViewer,
|
||||||
clock: FractalClockApp,
|
clock: FractalClockApp,
|
||||||
color_test: ColorTestApp,
|
color_test: ColorTestApp,
|
||||||
|
|
||||||
@@ -161,6 +165,8 @@ pub struct WrapApp {
|
|||||||
|
|
||||||
impl WrapApp {
|
impl WrapApp {
|
||||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
egui_extras::install_image_loaders(&_cc.egui_ctx);
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut slf = Self {
|
let mut slf = Self {
|
||||||
state: State::default(),
|
state: State::default(),
|
||||||
@@ -204,6 +210,12 @@ impl WrapApp {
|
|||||||
Anchor::Clock,
|
Anchor::Clock,
|
||||||
&mut self.state.clock as &mut dyn eframe::App,
|
&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"))]
|
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||||
|
|||||||
@@ -28,16 +28,13 @@ chrono = ["egui_extras/datepicker", "dep:chrono"]
|
|||||||
serde = ["egui/serde", "egui_plot/serde", "dep:serde"]
|
serde = ["egui/serde", "egui_plot/serde", "dep:serde"]
|
||||||
|
|
||||||
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
|
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
|
||||||
syntax_highlighting = ["syntect"]
|
syntect = ["egui_extras/syntect"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.22.0", path = "../egui", default-features = false }
|
egui = { version = "0.22.0", path = "../egui", default-features = false }
|
||||||
egui_extras = { version = "0.22.0", path = "../egui_extras", features = [
|
egui_extras = { version = "0.22.0", path = "../egui_extras" }
|
||||||
"log",
|
|
||||||
] }
|
|
||||||
egui_plot = { version = "0.22.0", path = "../egui_plot" }
|
egui_plot = { version = "0.22.0", path = "../egui_plot" }
|
||||||
enum-map = { version = "2", features = ["serde"] }
|
|
||||||
log = { version = "0.4", features = ["std"] }
|
log = { version = "0.4", features = ["std"] }
|
||||||
unicode_names2 = { version = "0.6.0", default-features = false }
|
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.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
document-features = { version = "0.2", optional = true }
|
||||||
serde = { version = "1", optional = true, features = ["derive"] }
|
serde = { version = "1", optional = true, features = ["derive"] }
|
||||||
syntect = { version = "5", optional = true, default-features = false, features = [
|
|
||||||
"default-fancy",
|
|
||||||
] }
|
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[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 tex = self.tex_mngr.get(ui.ctx(), &g);
|
||||||
let texel_offset = 0.5 / (g.0.len() as f32);
|
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));
|
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))
|
ui.add(
|
||||||
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
|
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");
|
ui.label("GPU result");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -225,11 +229,15 @@ impl ColorTest {
|
|||||||
let tex = self.tex_mngr.get(ui.ctx(), gradient);
|
let tex = self.tex_mngr.get(ui.ctx(), gradient);
|
||||||
let texel_offset = 0.5 / (gradient.0.len() as f32);
|
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));
|
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))
|
ui.add(
|
||||||
.on_hover_text(format!(
|
Image::from_texture((tex.id(), GRADIENT_SIZE))
|
||||||
"A texture that is {} texels wide",
|
.bg_fill(bg_fill)
|
||||||
gradient.0.len()
|
.uv(uv),
|
||||||
));
|
)
|
||||||
|
.on_hover_text(format!(
|
||||||
|
"A texture that is {} texels wide",
|
||||||
|
gradient.0.len()
|
||||||
|
));
|
||||||
ui.label(label);
|
ui.label(label);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ impl super::View for About {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn about_immediate_mode(ui: &mut egui::Ui) {
|
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.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
|
||||||
|
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
@@ -56,7 +55,7 @@ fn about_immediate_mode(ui: &mut egui::Ui) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
code_view_ui(
|
crate::rust_view_ui(
|
||||||
ui,
|
ui,
|
||||||
r#"
|
r#"
|
||||||
if ui.button("Save").clicked() {
|
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.collapsing("Theme", |ui| {
|
||||||
ui.group(|ui| {
|
ui.group(|ui| {
|
||||||
theme.ui(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 layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||||
let mut layout_job =
|
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;
|
layout_job.wrap.max_width = wrap_width;
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -81,13 +81,11 @@ impl super::Demo for CodeExample {
|
|||||||
|
|
||||||
impl super::View for CodeExample {
|
impl super::View for CodeExample {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
use crate::syntax_highlighting::code_view_ui;
|
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add(crate::egui_github_link_file!());
|
ui.add(crate::egui_github_link_file!());
|
||||||
});
|
});
|
||||||
|
|
||||||
code_view_ui(
|
crate::rust_view_ui(
|
||||||
ui,
|
ui,
|
||||||
r"
|
r"
|
||||||
pub struct CodeExample {
|
pub struct CodeExample {
|
||||||
@@ -117,15 +115,15 @@ impl CodeExample {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
code_view_ui(ui, " }\n}");
|
crate::rust_view_ui(ui, " }\n}");
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
code_view_ui(ui, &format!("{self:#?}"));
|
crate::rust_view_ui(ui, &format!("{self:#?}"));
|
||||||
|
|
||||||
ui.separator();
|
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| {
|
ui.collapsing("Theme", |ui| {
|
||||||
theme.ui(ui);
|
theme.ui(ui);
|
||||||
theme.store_in_memory(ui.ctx());
|
theme.store_in_memory(ui.ctx());
|
||||||
@@ -135,7 +133,7 @@ impl CodeExample {
|
|||||||
|
|
||||||
fn show_code(ui: &mut egui::Ui, code: &str) {
|
fn show_code(ui: &mut egui::Ui, code: &str) {
|
||||||
let code = remove_leading_indentation(code.trim_start_matches('\n'));
|
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 {
|
fn remove_leading_indentation(code: &str) -> String {
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ pub struct WidgetGallery {
|
|||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
date: Option<chrono::NaiveDate>,
|
date: Option<chrono::NaiveDate>,
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
|
||||||
texture: Option<egui::TextureHandle>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WidgetGallery {
|
impl Default for WidgetGallery {
|
||||||
@@ -39,7 +36,6 @@ impl Default for WidgetGallery {
|
|||||||
animate_progress_bar: false,
|
animate_progress_bar: false,
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
date: None,
|
date: None,
|
||||||
texture: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,14 +107,8 @@ impl WidgetGallery {
|
|||||||
animate_progress_bar,
|
animate_progress_bar,
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
date,
|
date,
|
||||||
texture,
|
|
||||||
} = self;
|
} = 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.add(doc_link_label("Label", "label,heading"));
|
||||||
ui.label("Welcome to the widget gallery!");
|
ui.label("Welcome to the widget gallery!");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
@@ -206,14 +196,19 @@ impl WidgetGallery {
|
|||||||
ui.color_edit_button_srgba(color);
|
ui.color_edit_button_srgba(color);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
|
|
||||||
|
|
||||||
ui.add(doc_link_label("Image", "Image"));
|
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.end_row();
|
||||||
|
|
||||||
ui.add(doc_link_label("ImageButton", "ImageButton"));
|
ui.add(doc_link_label(
|
||||||
if ui.add(egui::ImageButton::new(texture, img_size)).clicked() {
|
"Button with image",
|
||||||
|
"Button::image_and_text",
|
||||||
|
));
|
||||||
|
if ui
|
||||||
|
.add(egui::Button::image_and_text(egui_icon, "Click me!"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
*boolean = !*boolean;
|
*boolean = !*boolean;
|
||||||
}
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|||||||
@@ -15,13 +15,19 @@
|
|||||||
mod color_test;
|
mod color_test;
|
||||||
mod demo;
|
mod demo;
|
||||||
pub mod easy_mark;
|
pub mod easy_mark;
|
||||||
pub mod syntax_highlighting;
|
|
||||||
|
|
||||||
pub use color_test::ColorTest;
|
pub use color_test::ColorTest;
|
||||||
pub use demo::DemoWindows;
|
pub use demo::DemoWindows;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use egui::ViewportId;
|
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.
|
/// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file on github.
|
||||||
|
|||||||
@@ -24,20 +24,28 @@ all-features = true
|
|||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["dep:mime_guess"]
|
||||||
|
|
||||||
## Shorthand for enabling `svg`, `image`, and `ehttp`.
|
## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`).
|
||||||
all-loaders = ["svg", "image", "http"]
|
all_loaders = ["file", "http", "image", "svg"]
|
||||||
|
|
||||||
## Enable [`DatePickerButton`] widget.
|
## Enable [`DatePickerButton`] widget.
|
||||||
datepicker = ["chrono"]
|
datepicker = ["chrono"]
|
||||||
|
|
||||||
## Log warnings using [`log`](https://docs.rs/log) crate.
|
## Add support for loading images from `file://` URIs.
|
||||||
log = ["dep:log", "egui/log"]
|
file = ["dep:mime_guess"]
|
||||||
|
|
||||||
## Add support for loading images via HTTP.
|
## Add support for loading images via HTTP.
|
||||||
http = ["dep:ehttp"]
|
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.
|
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
|
||||||
##
|
##
|
||||||
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
|
## 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.
|
## Support loading svg images.
|
||||||
svg = ["resvg", "tiny-skia", "usvg"]
|
svg = ["resvg", "tiny-skia", "usvg"]
|
||||||
|
|
||||||
[dependencies]
|
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
|
||||||
egui = { version = "0.22.0", path = "../egui", default-features = false }
|
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"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### Optional dependencies
|
||||||
@@ -64,19 +79,17 @@ chrono = { version = "0.4", optional = true, default-features = false, features
|
|||||||
## Enable this when generating docs.
|
## Enable this when generating docs.
|
||||||
document-features = { version = "0.2", optional = true }
|
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 }
|
image = { version = "0.24", optional = true, default-features = false }
|
||||||
|
|
||||||
# feature "log"
|
# file feature
|
||||||
log = { version = "0.4", optional = true, features = ["std"] }
|
mime_guess = { version = "2.0.4", optional = true, default-features = false }
|
||||||
|
|
||||||
puffin = { version = "0.16", optional = true }
|
puffin = { version = "0.16", optional = true }
|
||||||
|
|
||||||
|
syntect = { version = "5", optional = true, default-features = false, features = [
|
||||||
|
"default-fancy",
|
||||||
|
] }
|
||||||
|
|
||||||
# svg feature
|
# svg feature
|
||||||
resvg = { version = "0.28", optional = true, default-features = false }
|
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
|
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};
|
use egui::{mutex::Mutex, TextureFilter, TextureOptions};
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
@@ -8,6 +10,9 @@ pub use usvg::FitTo;
|
|||||||
/// Load once, and save somewhere in your app state.
|
/// Load once, and save somewhere in your app state.
|
||||||
///
|
///
|
||||||
/// Use the `svg` and `image` features to enable more constructors.
|
/// 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 {
|
pub struct RetainedImage {
|
||||||
debug_name: String,
|
debug_name: String,
|
||||||
|
|
||||||
@@ -151,7 +156,7 @@ impl RetainedImage {
|
|||||||
&self.debug_name
|
&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 {
|
pub fn texture_id(&self, ctx: &egui::Context) -> egui::TextureId {
|
||||||
self.texture
|
self.texture
|
||||||
.lock()
|
.lock()
|
||||||
@@ -186,7 +191,7 @@ impl RetainedImage {
|
|||||||
// We need to convert the SVG to a texture to display it:
|
// We need to convert the SVG to a texture to display it:
|
||||||
// Future improvement: tell backend to do mip-mapping of the image to
|
// Future improvement: tell backend to do mip-mapping of the image to
|
||||||
// make it look smoother when downsized.
|
// 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")]
|
#[cfg(feature = "chrono")]
|
||||||
mod datepicker;
|
mod datepicker;
|
||||||
|
|
||||||
|
pub mod syntax_highlighting;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
pub mod image;
|
pub mod image;
|
||||||
mod layout;
|
mod layout;
|
||||||
pub mod loaders;
|
pub mod loaders;
|
||||||
@@ -23,12 +26,16 @@ mod table;
|
|||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
pub use crate::datepicker::DatePickerButton;
|
pub use crate::datepicker::DatePickerButton;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(deprecated)]
|
||||||
pub use crate::image::RetainedImage;
|
pub use crate::image::RetainedImage;
|
||||||
pub(crate) use crate::layout::StripLayout;
|
pub(crate) use crate::layout::StripLayout;
|
||||||
pub use crate::sizing::Size;
|
pub use crate::sizing::Size;
|
||||||
pub use crate::strip::*;
|
pub use crate::strip::*;
|
||||||
pub use crate::table::*;
|
pub use crate::table::*;
|
||||||
|
|
||||||
|
pub use loaders::install_image_loaders;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
mod profiling_scopes {
|
mod profiling_scopes {
|
||||||
@@ -38,8 +45,8 @@ mod profiling_scopes {
|
|||||||
/// Profiling macro for feature "puffin"
|
/// Profiling macro for feature "puffin"
|
||||||
macro_rules! profile_function {
|
macro_rules! profile_function {
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
|
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
||||||
puffin::profile_function!($($arg)*);
|
puffin::profile_function!($($arg)*);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -48,8 +55,8 @@ mod profiling_scopes {
|
|||||||
/// Profiling macro for feature "puffin"
|
/// Profiling macro for feature "puffin"
|
||||||
macro_rules! profile_scope {
|
macro_rules! profile_scope {
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
|
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
||||||
puffin::profile_scope!($($arg)*);
|
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.
|
/// Panic in debug builds, log otherwise.
|
||||||
macro_rules! log_or_panic {
|
macro_rules! log_or_panic {
|
||||||
|
($fmt: literal) => {$crate::log_or_panic!($fmt,)};
|
||||||
($fmt: literal, $($arg: tt)*) => {{
|
($fmt: literal, $($arg: tt)*) => {{
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
panic!($fmt, $($arg)*);
|
panic!($fmt, $($arg)*);
|
||||||
} else {
|
} else {
|
||||||
$crate::log_err!($fmt, $($arg)*);
|
log::error!($fmt, $($arg)*);
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
pub(crate) use log_or_panic;
|
pub(crate) use log_or_panic;
|
||||||
|
|
||||||
#[allow(unused_macros)]
|
|
||||||
macro_rules! log_warn {
|
|
||||||
($fmt: literal) => {$crate::log_warn!($fmt,)};
|
|
||||||
($fmt: literal, $($arg: tt)*) => {{
|
|
||||||
#[cfg(feature = "log")]
|
|
||||||
log::warn!($fmt, $($arg)*);
|
|
||||||
|
|
||||||
#[cfg(not(feature = "log"))]
|
|
||||||
println!(
|
|
||||||
concat!("egui_extras: warning: ", $fmt), $($arg)*
|
|
||||||
)
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
pub(crate) use log_warn;
|
|
||||||
|
|
||||||
#[allow(unused_macros)]
|
|
||||||
macro_rules! log_trace {
|
|
||||||
($fmt: literal) => {$crate::log_trace!($fmt,)};
|
|
||||||
($fmt: literal, $($arg: tt)*) => {{
|
|
||||||
#[cfg(feature = "log")]
|
|
||||||
log::trace!($fmt, $($arg)*);
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
pub(crate) use log_trace;
|
|
||||||
|
|||||||
@@ -1,46 +1,96 @@
|
|||||||
// TODO: automatic cache eviction
|
// TODO: automatic cache eviction
|
||||||
|
|
||||||
/// Installs the default set of loaders:
|
/// Installs a set of image loaders.
|
||||||
/// - `file` loader on non-Wasm targets
|
|
||||||
/// - `http` loader (with the `ehttp` feature)
|
|
||||||
/// - `image` loader (with the `image` feature)
|
|
||||||
/// - `svg` loader with the `svg` feature
|
|
||||||
///
|
///
|
||||||
/// ⚠ This will do nothing and you won't see any images unless you enable some features!
|
/// Calling this enables the use of [`egui::Image`] and [`egui::Ui::image`].
|
||||||
/// If you just want to be able to load `file://` and `http://` images, enable the `all-loaders` feature.
|
|
||||||
///
|
///
|
||||||
/// ⚠ The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image)
|
/// ⚠ 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:
|
/// crate as your direct dependency, and enabling features on it:
|
||||||
///
|
///
|
||||||
/// ```toml,ignore
|
/// ```toml,ignore
|
||||||
|
/// egui_extras = { version = "*", features = ["all_loaders"] }
|
||||||
/// image = { version = "0.24", features = ["jpeg", "png"] }
|
/// 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.
|
/// See [`egui::load`] for more information about how loaders work.
|
||||||
pub fn install(ctx: &egui::Context) {
|
pub fn install_image_loaders(ctx: &egui::Context) {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(all(not(target_arch = "wasm32"), feature = "file"))]
|
||||||
ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default()));
|
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")]
|
#[cfg(feature = "http")]
|
||||||
ctx.add_bytes_loader(std::sync::Arc::new(
|
if !ctx.is_loader_installed(self::ehttp_loader::EhttpLoader::ID) {
|
||||||
self::ehttp_loader::EhttpLoader::default(),
|
ctx.add_bytes_loader(std::sync::Arc::new(
|
||||||
));
|
self::ehttp_loader::EhttpLoader::default(),
|
||||||
|
));
|
||||||
|
log::trace!("installed EhttpLoader");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
ctx.add_image_loader(std::sync::Arc::new(
|
if !ctx.is_loader_installed(self::image_loader::ImageCrateLoader::ID) {
|
||||||
self::image_loader::ImageCrateLoader::default(),
|
ctx.add_image_loader(std::sync::Arc::new(
|
||||||
));
|
self::image_loader::ImageCrateLoader::default(),
|
||||||
|
));
|
||||||
|
log::trace!("installed ImageCrateLoader");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
#[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(
|
#[cfg(all(
|
||||||
target_arch = "wasm32",
|
any(target_arch = "wasm32", not(feature = "file")),
|
||||||
not(feature = "http"),
|
not(feature = "http"),
|
||||||
not(feature = "image"),
|
not(feature = "image"),
|
||||||
not(feature = "svg")
|
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;
|
let _ = ctx;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,52 +5,60 @@ use egui::{
|
|||||||
};
|
};
|
||||||
use std::{sync::Arc, task::Poll};
|
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)]
|
#[derive(Default)]
|
||||||
pub struct EhttpLoader {
|
pub struct EhttpLoader {
|
||||||
cache: Arc<Mutex<HashMap<String, Entry>>>,
|
cache: Arc<Mutex<HashMap<String, Entry>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EhttpLoader {
|
||||||
|
pub const ID: &str = egui::generate_loader_id!(EhttpLoader);
|
||||||
|
}
|
||||||
|
|
||||||
const PROTOCOLS: &[&str] = &["http://", "https://"];
|
const PROTOCOLS: &[&str] = &["http://", "https://"];
|
||||||
|
|
||||||
fn starts_with_one_of(s: &str, prefixes: &[&str]) -> bool {
|
fn starts_with_one_of(s: &str, prefixes: &[&str]) -> bool {
|
||||||
prefixes.iter().any(|prefix| s.starts_with(prefix))
|
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 {
|
impl BytesLoader for EhttpLoader {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
Self::ID
|
||||||
|
}
|
||||||
|
|
||||||
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
|
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
|
||||||
if !starts_with_one_of(uri, PROTOCOLS) {
|
if !starts_with_one_of(uri, PROTOCOLS) {
|
||||||
return Err(LoadError::NotSupported);
|
return Err(LoadError::NotSupported);
|
||||||
@@ -59,15 +67,16 @@ impl BytesLoader for EhttpLoader {
|
|||||||
let mut cache = self.cache.lock();
|
let mut cache = self.cache.lock();
|
||||||
if let Some(entry) = cache.get(uri).cloned() {
|
if let Some(entry) = cache.get(uri).cloned() {
|
||||||
match entry {
|
match entry {
|
||||||
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
|
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
|
||||||
size: None,
|
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 }),
|
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
crate::log_trace!("started loading {uri:?}");
|
log::trace!("started loading {uri:?}");
|
||||||
|
|
||||||
let uri = uri.to_owned();
|
let uri = uri.to_owned();
|
||||||
cache.insert(uri.clone(), Poll::Pending);
|
cache.insert(uri.clone(), Poll::Pending);
|
||||||
@@ -77,8 +86,15 @@ impl BytesLoader for EhttpLoader {
|
|||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
let cache = self.cache.clone();
|
let cache = self.cache.clone();
|
||||||
move |response| {
|
move |response| {
|
||||||
let result = get_image_bytes(&uri, response);
|
let result = match response {
|
||||||
crate::log_trace!("finished loading {uri:?}");
|
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));
|
let prev = cache.lock().insert(uri, Poll::Ready(result));
|
||||||
assert!(matches!(prev, Some(Poll::Pending)));
|
assert!(matches!(prev, Some(Poll::Pending)));
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
@@ -93,12 +109,18 @@ impl BytesLoader for EhttpLoader {
|
|||||||
let _ = self.cache.lock().remove(uri);
|
let _ = self.cache.lock().remove(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn forget_all(&self) {
|
||||||
|
self.cache.lock().clear();
|
||||||
|
}
|
||||||
|
|
||||||
fn byte_size(&self) -> usize {
|
fn byte_size(&self) -> usize {
|
||||||
self.cache
|
self.cache
|
||||||
.lock()
|
.lock()
|
||||||
.values()
|
.values()
|
||||||
.map(|entry| match entry {
|
.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(),
|
Poll::Ready(Err(err)) => err.len(),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ use egui::{
|
|||||||
};
|
};
|
||||||
use std::{sync::Arc, task::Poll, thread};
|
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)]
|
#[derive(Default)]
|
||||||
pub struct FileLoader {
|
pub struct FileLoader {
|
||||||
@@ -13,9 +19,17 @@ pub struct FileLoader {
|
|||||||
cache: Arc<Mutex<HashMap<String, Entry>>>,
|
cache: Arc<Mutex<HashMap<String, Entry>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FileLoader {
|
||||||
|
pub const ID: &str = egui::generate_loader_id!(FileLoader);
|
||||||
|
}
|
||||||
|
|
||||||
const PROTOCOL: &str = "file://";
|
const PROTOCOL: &str = "file://";
|
||||||
|
|
||||||
impl BytesLoader for FileLoader {
|
impl BytesLoader for FileLoader {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
Self::ID
|
||||||
|
}
|
||||||
|
|
||||||
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
|
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
|
||||||
// File loader only supports the `file` protocol.
|
// File loader only supports the `file` protocol.
|
||||||
let Some(path) = uri.strip_prefix(PROTOCOL) else {
|
let Some(path) = uri.strip_prefix(PROTOCOL) else {
|
||||||
@@ -26,15 +40,16 @@ impl BytesLoader for FileLoader {
|
|||||||
if let Some(entry) = cache.get(path).cloned() {
|
if let Some(entry) = cache.get(path).cloned() {
|
||||||
// `path` has either begun loading, is loaded, or has failed to load.
|
// `path` has either begun loading, is loaded, or has failed to load.
|
||||||
match entry {
|
match entry {
|
||||||
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
|
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
|
||||||
size: None,
|
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 }),
|
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
crate::log_trace!("started loading {uri:?}");
|
log::trace!("started loading {uri:?}");
|
||||||
// We need to load the file at `path`.
|
// We need to load the file at `path`.
|
||||||
|
|
||||||
// Set the file to `pending` until we finish loading it.
|
// Set the file to `pending` until we finish loading it.
|
||||||
@@ -48,16 +63,29 @@ impl BytesLoader for FileLoader {
|
|||||||
.spawn({
|
.spawn({
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
let cache = self.cache.clone();
|
let cache = self.cache.clone();
|
||||||
let uri = uri.to_owned();
|
let _uri = uri.to_owned();
|
||||||
move || {
|
move || {
|
||||||
let result = match std::fs::read(&path) {
|
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()),
|
Err(err) => Err(err.to_string()),
|
||||||
};
|
};
|
||||||
let prev = cache.lock().insert(path, Poll::Ready(result));
|
let prev = cache.lock().insert(path, Poll::Ready(result));
|
||||||
assert!(matches!(prev, Some(Poll::Pending)));
|
assert!(matches!(prev, Some(Poll::Pending)));
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
crate::log_trace!("finished loading {uri:?}");
|
log::trace!("finished loading {_uri:?}");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.expect("failed to spawn thread");
|
.expect("failed to spawn thread");
|
||||||
@@ -70,12 +98,18 @@ impl BytesLoader for FileLoader {
|
|||||||
let _ = self.cache.lock().remove(uri);
|
let _ = self.cache.lock().remove(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn forget_all(&self) {
|
||||||
|
self.cache.lock().clear();
|
||||||
|
}
|
||||||
|
|
||||||
fn byte_size(&self) -> usize {
|
fn byte_size(&self) -> usize {
|
||||||
self.cache
|
self.cache
|
||||||
.lock()
|
.lock()
|
||||||
.values()
|
.values()
|
||||||
.map(|entry| match entry {
|
.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(),
|
Poll::Ready(Err(err)) => err.len(),
|
||||||
_ => 0,
|
_ => 0,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,18 +13,36 @@ pub struct ImageCrateLoader {
|
|||||||
cache: Mutex<HashMap<String, Entry>>,
|
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 {
|
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
|
// `true` because if there's no extension, assume that we support it
|
||||||
return true
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
ext != "svg"
|
ext != "svg"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_unsupported_mime(mime: &str) -> bool {
|
||||||
|
mime.contains("svg")
|
||||||
|
}
|
||||||
|
|
||||||
impl ImageLoader for ImageCrateLoader {
|
impl ImageLoader for ImageCrateLoader {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
Self::ID
|
||||||
|
}
|
||||||
|
|
||||||
fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult {
|
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);
|
return Err(LoadError::NotSupported);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,18 +50,25 @@ impl ImageLoader for ImageCrateLoader {
|
|||||||
if let Some(entry) = cache.get(uri).cloned() {
|
if let Some(entry) = cache.get(uri).cloned() {
|
||||||
match entry {
|
match entry {
|
||||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
Ok(image) => Ok(ImagePoll::Ready { image }),
|
||||||
Err(err) => Err(LoadError::Custom(err)),
|
Err(err) => Err(LoadError::Loading(err)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match ctx.try_load_bytes(uri) {
|
match ctx.try_load_bytes(uri) {
|
||||||
Ok(BytesPoll::Ready { bytes, .. }) => {
|
Ok(BytesPoll::Ready { bytes, mime, .. }) => {
|
||||||
crate::log_trace!("started loading {uri:?}");
|
// (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);
|
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());
|
cache.insert(uri.into(), result.clone());
|
||||||
match result {
|
match result {
|
||||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
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 }),
|
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
|
||||||
@@ -56,6 +81,10 @@ impl ImageLoader for ImageCrateLoader {
|
|||||||
let _ = self.cache.lock().remove(uri);
|
let _ = self.cache.lock().remove(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn forget_all(&self) {
|
||||||
|
self.cache.lock().clear();
|
||||||
|
}
|
||||||
|
|
||||||
fn byte_size(&self) -> usize {
|
fn byte_size(&self) -> usize {
|
||||||
self.cache
|
self.cache
|
||||||
.lock()
|
.lock()
|
||||||
@@ -74,11 +103,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_support() {
|
fn check_support() {
|
||||||
assert!(is_supported("https://test.png"));
|
assert!(is_supported_uri("https://test.png"));
|
||||||
assert!(is_supported("test.jpeg"));
|
assert!(is_supported_uri("test.jpeg"));
|
||||||
assert!(is_supported("http://test.gif"));
|
assert!(is_supported_uri("http://test.gif"));
|
||||||
assert!(is_supported("test.webp"));
|
assert!(is_supported_uri("test.webp"));
|
||||||
assert!(is_supported("file://test"));
|
assert!(is_supported_uri("file://test"));
|
||||||
assert!(!is_supported("test.svg"));
|
assert!(!is_supported_uri("test.svg"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,23 @@ pub struct SvgLoader {
|
|||||||
cache: Mutex<HashMap<(String, SizeHint), Entry>>,
|
cache: Mutex<HashMap<(String, SizeHint), Entry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SvgLoader {
|
||||||
|
pub const ID: &str = egui::generate_loader_id!(SvgLoader);
|
||||||
|
}
|
||||||
|
|
||||||
fn is_supported(uri: &str) -> bool {
|
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"
|
ext == "svg"
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageLoader for SvgLoader {
|
impl ImageLoader for SvgLoader {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
Self::ID
|
||||||
|
}
|
||||||
|
|
||||||
fn load(&self, ctx: &egui::Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult {
|
fn load(&self, ctx: &egui::Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult {
|
||||||
if !is_supported(uri) {
|
if !is_supported(uri) {
|
||||||
return Err(LoadError::NotSupported);
|
return Err(LoadError::NotSupported);
|
||||||
@@ -32,25 +42,25 @@ impl ImageLoader for SvgLoader {
|
|||||||
if let Some(entry) = cache.get(&(uri.clone(), size_hint)).cloned() {
|
if let Some(entry) = cache.get(&(uri.clone(), size_hint)).cloned() {
|
||||||
match entry {
|
match entry {
|
||||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
Ok(image) => Ok(ImagePoll::Ready { image }),
|
||||||
Err(err) => Err(LoadError::Custom(err)),
|
Err(err) => Err(LoadError::Loading(err)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match ctx.try_load_bytes(&uri) {
|
match ctx.try_load_bytes(&uri) {
|
||||||
Ok(BytesPoll::Ready { bytes, .. }) => {
|
Ok(BytesPoll::Ready { bytes, .. }) => {
|
||||||
crate::log_trace!("started loading {uri:?}");
|
log::trace!("started loading {uri:?}");
|
||||||
let fit_to = match size_hint {
|
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::Width(w) => usvg::FitTo::Width(w),
|
||||||
SizeHint::Height(h) => usvg::FitTo::Height(h),
|
SizeHint::Height(h) => usvg::FitTo::Height(h),
|
||||||
SizeHint::Size(w, h) => usvg::FitTo::Size(w, h),
|
SizeHint::Size(w, h) => usvg::FitTo::Size(w, h),
|
||||||
};
|
};
|
||||||
let result =
|
let result =
|
||||||
crate::image::load_svg_bytes_with_size(&bytes, fit_to).map(Arc::new);
|
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());
|
cache.insert((uri, size_hint), result.clone());
|
||||||
match result {
|
match result {
|
||||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
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 }),
|
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
|
||||||
@@ -63,6 +73,10 @@ impl ImageLoader for SvgLoader {
|
|||||||
self.cache.lock().retain(|(u, _), _| u != uri);
|
self.cache.lock().retain(|(u, _), _| u != uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn forget_all(&self) {
|
||||||
|
self.cache.lock().clear();
|
||||||
|
}
|
||||||
|
|
||||||
fn byte_size(&self) -> usize {
|
fn byte_size(&self) -> usize {
|
||||||
self.cache
|
self.cache
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ impl From<Vec<Size>> for Sizing {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_sizing() {
|
fn test_sizing() {
|
||||||
let sizing: Sizing = vec![].into();
|
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();
|
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]);
|
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;
|
use egui::text::LayoutJob;
|
||||||
|
|
||||||
/// View some code with syntax highlighting and selection.
|
/// View some code with syntax highlighting and selection.
|
||||||
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
|
pub fn code_view_ui(
|
||||||
let language = "rs";
|
ui: &mut egui::Ui,
|
||||||
let theme = CodeTheme::from_memory(ui.ctx());
|
theme: &CodeTheme,
|
||||||
|
mut code: &str,
|
||||||
|
language: &str,
|
||||||
|
) -> egui::Response {
|
||||||
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
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
|
// layout_job.wrap.max_width = wrap_width; // no wrapping
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
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)
|
.desired_rows(1)
|
||||||
.lock_focus(true)
|
.lock_focus(true)
|
||||||
.layouter(&mut layouter),
|
.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 {
|
pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
|
||||||
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
||||||
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
|
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"))]
|
#[cfg(not(feature = "syntect"))]
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq, serde::Deserialize, serde::Serialize, enum_map::Enum)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[derive(enum_map::Enum)]
|
|
||||||
enum TokenType {
|
enum TokenType {
|
||||||
Comment,
|
Comment,
|
||||||
Keyword,
|
Keyword,
|
||||||
@@ -54,8 +61,7 @@ enum TokenType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
#[derive(Clone, Copy, Hash, PartialEq)]
|
#[derive(Clone, Copy, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
enum SyntectTheme {
|
enum SyntectTheme {
|
||||||
Base16EightiesDark,
|
Base16EightiesDark,
|
||||||
Base16MochaDark,
|
Base16MochaDark,
|
||||||
@@ -118,9 +124,9 @@ impl SyntectTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Hash, PartialEq)]
|
/// A selected color theme.
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[derive(Clone, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[serde(default)]
|
||||||
pub struct CodeTheme {
|
pub struct CodeTheme {
|
||||||
dark_mode: bool,
|
dark_mode: bool,
|
||||||
|
|
||||||
@@ -138,6 +144,7 @@ impl Default for CodeTheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CodeTheme {
|
impl CodeTheme {
|
||||||
|
/// Selects either dark or light theme based on the given style.
|
||||||
pub fn from_style(style: &egui::Style) -> Self {
|
pub fn from_style(style: &egui::Style) -> Self {
|
||||||
if style.visuals.dark_mode {
|
if style.visuals.dark_mode {
|
||||||
Self::dark()
|
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 {
|
pub fn from_memory(ctx: &egui::Context) -> Self {
|
||||||
if ctx.style().visuals.dark_mode {
|
if ctx.style().visuals.dark_mode {
|
||||||
ctx.data_mut(|d| {
|
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) {
|
pub fn store_in_memory(self, ctx: &egui::Context) {
|
||||||
if self.dark_mode {
|
if self.dark_mode {
|
||||||
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self));
|
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) {
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
egui::widgets::global_dark_light_mode_buttons(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) {
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.horizontal_top(|ui| {
|
ui.horizontal_top(|ui| {
|
||||||
let selected_id = egui::Id::null();
|
let selected_id = egui::Id::null();
|
||||||
@@ -306,6 +321,7 @@ struct Highlighter {
|
|||||||
#[cfg(feature = "syntect")]
|
#[cfg(feature = "syntect")]
|
||||||
impl Default for Highlighter {
|
impl Default for Highlighter {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
crate::profile_function!();
|
||||||
Self {
|
Self {
|
||||||
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
||||||
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
||||||
@@ -313,7 +329,6 @@ impl Default for Highlighter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||||
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
|
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> {
|
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
||||||
|
crate::profile_function!();
|
||||||
|
|
||||||
use syntect::easy::HighlightLines;
|
use syntect::easy::HighlightLines;
|
||||||
use syntect::highlighting::FontStyle;
|
use syntect::highlighting::FontStyle;
|
||||||
use syntect::util::LinesWithEndings;
|
use syntect::util::LinesWithEndings;
|
||||||
@@ -400,13 +418,24 @@ struct Highlighter {}
|
|||||||
#[cfg(not(feature = "syntect"))]
|
#[cfg(not(feature = "syntect"))]
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
#[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
|
// Extremely simple syntax highlighter for when we compile without syntect
|
||||||
|
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
|
|
||||||
while !text.is_empty() {
|
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());
|
let end = text.find('\n').unwrap_or(text.len());
|
||||||
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
|
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
|
||||||
text = &text[end..];
|
text = &text[end..];
|
||||||
@@ -427,7 +456,7 @@ impl Highlighter {
|
|||||||
.find(|c: char| !c.is_ascii_alphanumeric())
|
.find(|c: char| !c.is_ascii_alphanumeric())
|
||||||
.map_or_else(|| text.len(), |i| i + 1);
|
.map_or_else(|| text.len(), |i| i + 1);
|
||||||
let word = &text[..end];
|
let word = &text[..end];
|
||||||
let tt = if is_keyword(word) {
|
let tt = if language.is_keyword(word) {
|
||||||
TokenType::Keyword
|
TokenType::Keyword
|
||||||
} else {
|
} else {
|
||||||
TokenType::Literal
|
TokenType::Literal
|
||||||
@@ -457,50 +486,173 @@ impl Highlighter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
job
|
Some(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
#[cfg(not(feature = "syntect"))]
|
||||||
fn is_keyword(word: &str) -> bool {
|
struct Language {
|
||||||
matches!(
|
/// `// comment`
|
||||||
word,
|
double_slash_comments: bool,
|
||||||
"as" | "async"
|
|
||||||
| "await"
|
/// `# comment`
|
||||||
| "break"
|
hash_comments: bool,
|
||||||
| "const"
|
|
||||||
| "continue"
|
keywords: std::collections::BTreeSet<&'static str>,
|
||||||
| "crate"
|
}
|
||||||
| "dyn"
|
|
||||||
| "else"
|
#[cfg(not(feature = "syntect"))]
|
||||||
| "enum"
|
impl Language {
|
||||||
| "extern"
|
fn new(language: &str) -> Option<Self> {
|
||||||
| "false"
|
match language.to_lowercase().as_str() {
|
||||||
| "fn"
|
"c" | "h" | "hpp" | "cpp" | "c++" => Some(Self::cpp()),
|
||||||
| "for"
|
"py" | "python" => Some(Self::python()),
|
||||||
| "if"
|
"rs" | "rust" => Some(Self::rust()),
|
||||||
| "impl"
|
_ => {
|
||||||
| "in"
|
None // unsupported language
|
||||||
| "let"
|
}
|
||||||
| "loop"
|
}
|
||||||
| "match"
|
}
|
||||||
| "mod"
|
|
||||||
| "move"
|
fn is_keyword(&self, word: &str) -> bool {
|
||||||
| "mut"
|
self.keywords.contains(word)
|
||||||
| "pub"
|
}
|
||||||
| "ref"
|
|
||||||
| "return"
|
fn cpp() -> Self {
|
||||||
| "self"
|
Self {
|
||||||
| "Self"
|
double_slash_comments: true,
|
||||||
| "static"
|
hash_comments: false,
|
||||||
| "struct"
|
keywords: [
|
||||||
| "super"
|
"alignas",
|
||||||
| "trait"
|
"alignof",
|
||||||
| "true"
|
"and_eq",
|
||||||
| "type"
|
"and",
|
||||||
| "unsafe"
|
"asm",
|
||||||
| "use"
|
"atomic_cancel",
|
||||||
| "where"
|
"atomic_commit",
|
||||||
| "while"
|
"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!
|
// If the last column is 'remainder', then let it fill the remainder!
|
||||||
let eps = 0.1; // just to avoid some rounding errors.
|
let eps = 0.1; // just to avoid some rounding errors.
|
||||||
*column_width = available_width - eps;
|
*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);
|
*column_width = width_range.clamp(*column_width);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ fn main() {
|
|||||||
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
|
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
|
||||||
if ui
|
if ui
|
||||||
.add(egui::Button::image_and_text(
|
.add(egui::Button::image_and_text(
|
||||||
texture_id,
|
(texture_id, button_image_size),
|
||||||
button_image_size,
|
|
||||||
"Quit",
|
"Quit",
|
||||||
))
|
))
|
||||||
.clicked()
|
.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"
|
mod profiling_scopes {
|
||||||
macro_rules! profile_function {
|
#![allow(unused_macros)]
|
||||||
($($arg: tt)*) => {
|
#![allow(unused_imports)]
|
||||||
#[cfg(feature = "puffin")]
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
puffin::profile_function!($($arg)*);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
pub(crate) use profile_function;
|
|
||||||
|
|
||||||
/// Profiling macro for feature "puffin"
|
/// Profiling macro for feature "puffin"
|
||||||
macro_rules! profile_scope {
|
macro_rules! profile_function {
|
||||||
($($arg: tt)*) => {
|
($($arg: tt)*) => {
|
||||||
#[cfg(feature = "puffin")]
|
#[cfg(feature = "puffin")]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there.
|
||||||
puffin::profile_scope!($($arg)*);
|
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) => {
|
Primitive::Callback(callback) => {
|
||||||
if callback.rect.is_positive() {
|
if callback.rect.is_positive() {
|
||||||
crate::profile_scope!("callback");
|
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 {
|
let info = egui::PaintCallbackInfo {
|
||||||
viewport: callback.rect,
|
viewport: callback.rect,
|
||||||
@@ -397,6 +378,16 @@ impl Painter {
|
|||||||
screen_size_px,
|
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>() {
|
if let Some(callback) = callback.callback.downcast_ref::<CallbackFn>() {
|
||||||
(callback.f)(info, self);
|
(callback.f)(info, self);
|
||||||
} else {
|
} else {
|
||||||
@@ -494,10 +485,13 @@ impl Painter {
|
|||||||
"Mismatch between texture size and texel count"
|
"Mismatch between texture size and texel count"
|
||||||
);
|
);
|
||||||
|
|
||||||
let data: Vec<u8> = image
|
let data: Vec<u8> = {
|
||||||
.srgba_pixels(None)
|
crate::profile_scope!("font -> sRGBA");
|
||||||
.flat_map(|a| a.to_array())
|
image
|
||||||
.collect();
|
.srgba_pixels(None)
|
||||||
|
.flat_map(|a| a.to_array())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
self.upload_texture_srgb(delta.pos, image.size, delta.options, &data);
|
self.upload_texture_srgb(delta.pos, image.size, delta.options, &data);
|
||||||
}
|
}
|
||||||
@@ -511,6 +505,7 @@ impl Painter {
|
|||||||
options: egui::TextureOptions,
|
options: egui::TextureOptions,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) {
|
) {
|
||||||
|
crate::profile_function!();
|
||||||
assert_eq!(data.len(), w * h * 4);
|
assert_eq!(data.len(), w * h * 4);
|
||||||
assert!(
|
assert!(
|
||||||
w <= self.max_texture_side && h <= self.max_texture_side,
|
w <= self.max_texture_side && h <= self.max_texture_side,
|
||||||
@@ -561,6 +556,7 @@ impl Painter {
|
|||||||
|
|
||||||
let level = 0;
|
let level = 0;
|
||||||
if let Some([x, y]) = pos {
|
if let Some([x, y]) = pos {
|
||||||
|
crate::profile_scope!("gl.tex_sub_image_2d");
|
||||||
self.gl.tex_sub_image_2d(
|
self.gl.tex_sub_image_2d(
|
||||||
glow::TEXTURE_2D,
|
glow::TEXTURE_2D,
|
||||||
level,
|
level,
|
||||||
@@ -575,6 +571,7 @@ impl Painter {
|
|||||||
check_for_gl_error!(&self.gl, "tex_sub_image_2d");
|
check_for_gl_error!(&self.gl, "tex_sub_image_2d");
|
||||||
} else {
|
} else {
|
||||||
let border = 0;
|
let border = 0;
|
||||||
|
crate::profile_scope!("gl.tex_image_2d");
|
||||||
self.gl.tex_image_2d(
|
self.gl.tex_image_2d(
|
||||||
glow::TEXTURE_2D,
|
glow::TEXTURE_2D,
|
||||||
level,
|
level,
|
||||||
|
|||||||
@@ -1234,12 +1234,19 @@ impl PlotItem for PlotImage {
|
|||||||
Rect::from_two_pos(left_top_screen, right_bottom_screen)
|
Rect::from_two_pos(left_top_screen, right_bottom_screen)
|
||||||
};
|
};
|
||||||
let screen_rotation = -*rotation as f32;
|
let screen_rotation = -*rotation as f32;
|
||||||
Image::new(*texture_id, image_screen_rect.size())
|
|
||||||
.bg_fill(*bg_fill)
|
egui::paint_texture_at(
|
||||||
.tint(*tint)
|
ui.painter(),
|
||||||
.uv(*uv)
|
image_screen_rect,
|
||||||
.rotate(screen_rotation, Vec2::splat(0.5))
|
&ImageOptions {
|
||||||
.paint_at(ui, image_screen_rect);
|
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 {
|
if *highlight {
|
||||||
let center = image_screen_rect.center();
|
let center = image_screen_rect.center();
|
||||||
let rotation = Rot2::from_angle(screen_rotation);
|
let rotation = Rot2::from_angle(screen_rotation);
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ impl FontImage {
|
|||||||
/// `gamma` should normally be set to `None`.
|
/// `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`.
|
/// 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(
|
pub fn srgba_pixels(
|
||||||
&'_ self,
|
&'_ self,
|
||||||
gamma: Option<f32>,
|
gamma: Option<f32>,
|
||||||
@@ -338,6 +339,7 @@ impl From<FontImage> for ImageData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn fast_round(r: f32) -> u8 {
|
fn fast_round(r: f32) -> u8 {
|
||||||
(r + 0.5).floor() as _ // rust does a saturating cast since 1.45
|
(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.
|
/// 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`].
|
/// Note than only a portion of this may be visible due to [`Self::clip_rect`].
|
||||||
|
///
|
||||||
|
/// This comes from [`PaintCallback::rect`].
|
||||||
pub viewport: Rect,
|
pub viewport: Rect,
|
||||||
|
|
||||||
/// Clip rectangle in points.
|
/// Clip rectangle in points.
|
||||||
@@ -819,7 +821,7 @@ pub struct ViewportInPixels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PaintCallbackInfo {
|
impl PaintCallbackInfo {
|
||||||
fn points_to_pixels(&self, rect: &Rect) -> ViewportInPixels {
|
fn pixels_from_points(&self, rect: &Rect) -> ViewportInPixels {
|
||||||
ViewportInPixels {
|
ViewportInPixels {
|
||||||
left_px: rect.min.x * self.pixels_per_point,
|
left_px: rect.min.x * self.pixels_per_point,
|
||||||
top_px: rect.min.y * 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`.
|
/// The viewport rectangle. This is what you would use in e.g. `glViewport`.
|
||||||
pub fn viewport_in_pixels(&self) -> ViewportInPixels {
|
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`.
|
/// The "scissor" or "clip" rectangle. This is what you would use in e.g. `glScissor`.
|
||||||
pub fn clip_rect_in_pixels(&self) -> ViewportInPixels {
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct PaintCallback {
|
pub struct PaintCallback {
|
||||||
/// Where to paint.
|
/// Where to paint.
|
||||||
|
///
|
||||||
|
/// This will become [`PaintCallbackInfo::viewport`].
|
||||||
pub rect: Rect,
|
pub rect: Rect,
|
||||||
|
|
||||||
/// Paint something custom (e.g. 3D stuff).
|
/// 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`].
|
/// 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 {
|
pub struct Tessellator {
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
options: TessellationOptions,
|
options: TessellationOptions,
|
||||||
|
|||||||
@@ -43,12 +43,6 @@ pub struct GlyphInfo {
|
|||||||
/// Unit: points.
|
/// Unit: points.
|
||||||
pub advance_width: f32,
|
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.
|
/// Texture coordinates.
|
||||||
pub uv_rect: UvRect,
|
pub uv_rect: UvRect,
|
||||||
}
|
}
|
||||||
@@ -59,7 +53,6 @@ impl Default for GlyphInfo {
|
|||||||
Self {
|
Self {
|
||||||
id: ab_glyph::GlyphId(0),
|
id: ab_glyph::GlyphId(0),
|
||||||
advance_width: 0.0,
|
advance_width: 0.0,
|
||||||
ascent: 0.0,
|
|
||||||
uv_rect: Default::default(),
|
uv_rect: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +181,7 @@ impl FontImpl {
|
|||||||
if let Some(space) = self.glyph_info(' ') {
|
if let Some(space) = self.glyph_info(' ') {
|
||||||
let glyph_info = GlyphInfo {
|
let glyph_info = GlyphInfo {
|
||||||
advance_width: crate::text::TAB_SIZE as f32 * space.advance_width,
|
advance_width: crate::text::TAB_SIZE as f32 * space.advance_width,
|
||||||
..GlyphInfo::default()
|
..space
|
||||||
};
|
};
|
||||||
self.glyph_info_cache.write().insert(c, glyph_info);
|
self.glyph_info_cache.write().insert(c, glyph_info);
|
||||||
return Some(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 advance_width = f32::min(em / 6.0, space.advance_width * 0.5);
|
||||||
let glyph_info = GlyphInfo {
|
let glyph_info = GlyphInfo {
|
||||||
advance_width,
|
advance_width,
|
||||||
..GlyphInfo::default()
|
..space
|
||||||
};
|
};
|
||||||
self.glyph_info_cache.write().insert(c, glyph_info);
|
self.glyph_info_cache.write().insert(c, glyph_info);
|
||||||
return Some(glyph_info);
|
return Some(glyph_info);
|
||||||
@@ -255,6 +248,14 @@ impl FontImpl {
|
|||||||
self.pixels_per_point
|
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 {
|
fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo {
|
||||||
assert!(glyph_id.0 != 0);
|
assert!(glyph_id.0 != 0);
|
||||||
use ab_glyph::{Font as _, ScaleFont};
|
use ab_glyph::{Font as _, ScaleFont};
|
||||||
@@ -305,7 +306,6 @@ impl FontImpl {
|
|||||||
GlyphInfo {
|
GlyphInfo {
|
||||||
id: glyph_id,
|
id: glyph_id,
|
||||||
advance_width: advance_width_in_points,
|
advance_width: advance_width_in_points,
|
||||||
ascent: self.ascent,
|
|
||||||
uv_rect,
|
uv_rect,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,7 +442,7 @@ impl Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[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() {
|
if self.fonts.is_empty() {
|
||||||
return (None, self.replacement_glyph.1);
|
return (None, self.replacement_glyph.1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ fn layout_section(
|
|||||||
paragraph = out_paragraphs.last_mut().unwrap();
|
paragraph = out_paragraphs.last_mut().unwrap();
|
||||||
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
|
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
|
||||||
} else {
|
} 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(font_impl) = font_impl {
|
||||||
if let Some(last_glyph_id) = last_glyph_id {
|
if let Some(last_glyph_id) = last_glyph_id {
|
||||||
paragraph.cursor_x += font_impl.pair_kerning(last_glyph_id, glyph_info.id);
|
paragraph.cursor_x += font_impl.pair_kerning(last_glyph_id, glyph_info.id);
|
||||||
@@ -146,7 +146,7 @@ fn layout_section(
|
|||||||
chr,
|
chr,
|
||||||
pos: pos2(paragraph.cursor_x, f32::NAN),
|
pos: pos2(paragraph.cursor_x, f32::NAN),
|
||||||
size: vec2(glyph_info.advance_width, line_height),
|
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,
|
uv_rect: glyph_info.uv_rect,
|
||||||
section_index,
|
section_index,
|
||||||
});
|
});
|
||||||
@@ -340,12 +340,12 @@ fn replace_last_glyph_with_overflow_character(
|
|||||||
.unwrap_or_else(|| font.row_height());
|
.unwrap_or_else(|| font.row_height());
|
||||||
|
|
||||||
let prev_glyph_id = prev_glyph.map(|prev_glyph| {
|
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
|
prev_glyph_info.id
|
||||||
});
|
});
|
||||||
|
|
||||||
// undo kerning with previous glyph
|
// 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
|
last_glyph.pos.x -= extra_letter_spacing
|
||||||
+ font_impl
|
+ font_impl
|
||||||
.zip(prev_glyph_id)
|
.zip(prev_glyph_id)
|
||||||
@@ -356,10 +356,9 @@ fn replace_last_glyph_with_overflow_character(
|
|||||||
|
|
||||||
// replace the glyph
|
// replace the glyph
|
||||||
last_glyph.chr = overflow_character;
|
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.size = vec2(glyph_info.advance_width, line_height);
|
||||||
last_glyph.uv_rect = glyph_info.uv_rect;
|
last_glyph.uv_rect = glyph_info.uv_rect;
|
||||||
last_glyph.ascent = glyph_info.ascent;
|
|
||||||
|
|
||||||
// reapply kerning
|
// reapply kerning
|
||||||
last_glyph.pos.x += extra_letter_spacing
|
last_glyph.pos.x += extra_letter_spacing
|
||||||
|
|||||||
@@ -86,7 +86,10 @@ impl TextureHandle {
|
|||||||
|
|
||||||
/// width x height
|
/// width x height
|
||||||
pub fn size(&self) -> [usize; 2] {
|
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
|
/// width x height
|
||||||
@@ -97,7 +100,10 @@ impl TextureHandle {
|
|||||||
|
|
||||||
/// `width x height x bytes_per_pixel`
|
/// `width x height x bytes_per_pixel`
|
||||||
pub fn byte_size(&self) -> usize {
|
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
|
/// width / height
|
||||||
@@ -108,7 +114,10 @@ impl TextureHandle {
|
|||||||
|
|
||||||
/// Debug-name.
|
/// Debug-name.
|
||||||
pub fn name(&self) -> String {
|
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`.
|
/// Possible types for `T` are `f32` and `f64`.
|
||||||
///
|
///
|
||||||
/// See also [`FloatOrd`].
|
/// See also [`FloatOrd`].
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
pub struct OrderedFloat<T>(T);
|
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> Eq for OrderedFloat<T> {}
|
||||||
|
|
||||||
impl<T: Float> PartialEq<Self> 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]
|
[package]
|
||||||
name = "retained_image"
|
name = "images"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
authors = ["Jan Procházka <github.com/jprochazk>"]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
@@ -10,8 +10,11 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eframe = { path = "../../crates/eframe", features = [
|
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"
|
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) => {
|
Ok(puffin_server) => {
|
||||||
eprintln!("Run: cargo install puffin_viewer && puffin_viewer --url 127.0.0.1:8585");
|
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
|
// 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!
|
// it to keep running. Dropping it closes the server, so let's not drop it!
|
||||||
#[allow(clippy::mem_forget)]
|
#[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() {
|
if let Some(texture) = self.texture.as_ref() {
|
||||||
ui.image(texture, ui.available_size());
|
ui.image((texture.id(), ui.available_size()));
|
||||||
} else {
|
} else {
|
||||||
ui.spinner();
|
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
|
|
||||||