Polish image API (#3338)

* Imoprove docs for callback shapes

* Improve docs for loader traits

* Use snake_case for feature `all_loaders`

* Make loaders publix

* Slightly better error message on image load failure

* Improve image loading error messages

* Use `bytes://` schema for included bytes loader

* Try user loaders first

* Move `image_loading_spinners` to `Visuals`

* Unify and simplify code

* Make the main text of `Button` optional

This largely makes ImageButton obsolete

* Fix docstrings

* Better docs

* typos

* Use the more explicit `egui_extras::install_image_loaders`

* Simplify `Image::paint_at` function
This commit is contained in:
Emil Ernerfeldt 2023-09-14 16:33:10 +02:00 committed by GitHub
parent e367c20779
commit d7d222d3f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 406 additions and 279 deletions

View File

@ -445,6 +445,13 @@ impl Renderer {
needs_reset = true;
let info = PaintCallbackInfo {
viewport: callback.rect,
clip_rect: *clip_rect,
pixels_per_point,
screen_size_px: size_in_pixels,
};
{
// We're setting a default viewport for the render pass as a
// courtesy for the user, so that they don't have to think about
@ -455,29 +462,19 @@ impl Renderer {
// viewport during the paint callback, effectively overriding this
// one.
let min = (callback.rect.min.to_vec2() * pixels_per_point).round();
let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
let viewport_px = info.viewport_in_pixels();
render_pass.set_viewport(
min.x,
min.y,
max.x - min.x,
max.y - min.y,
viewport_px.left_px,
viewport_px.top_px,
viewport_px.width_px,
viewport_px.height_px,
0.0,
1.0,
);
}
cbfn.0.paint(
PaintCallbackInfo {
viewport: callback.rect,
clip_rect: *clip_rect,
pixels_per_point,
screen_size_px: size_in_pixels,
},
render_pass,
&self.callback_resources,
);
cbfn.0.paint(info, render_pass, &self.callback_resources);
}
}
}

View File

@ -1121,9 +1121,16 @@ impl Context {
/// Allocate a texture.
///
/// In order to display an image you must convert it to a texture using this function.
/// This is for advanced users.
/// Most users should use [`crate::Ui::image`] or [`Self::try_load_texture`]
/// instead.
///
/// Make sure to only call this once for each image, i.e. NOT in your main GUI code.
/// In order to display an image you must convert it to a texture using this function.
/// The function will hand over the image data to the egui backend, which will
/// upload it to the GPU.
///
/// ⚠️ Make sure to only call this ONCE for each image, i.e. NOT in your main GUI code.
/// The call is NOT immediate safe.
///
/// The given name can be useful for later debugging, and will be visible if you call [`Self::texture_ui`].
///
@ -1151,7 +1158,7 @@ impl Context {
/// }
/// ```
///
/// Se also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::ImageButton`].
/// See also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::Image`].
pub fn load_texture(
&self,
name: impl Into<String>,
@ -1912,6 +1919,9 @@ impl Context {
/// Associate some static bytes with a `uri`.
///
/// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image.
///
/// By convention, the `uri` should start with `bytes://`.
/// Following that convention will lead to better error messages.
pub fn include_bytes(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
self.loaders().include.insert(uri, bytes);
}
@ -1921,32 +1931,32 @@ impl Context {
pub fn is_loader_installed(&self, id: &str) -> bool {
let loaders = self.loaders();
let in_bytes = loaders.bytes.lock().iter().any(|loader| loader.id() == id);
let in_image = loaders.image.lock().iter().any(|loader| loader.id() == id);
let in_texture = loaders
.texture
.lock()
.iter()
.any(|loader| loader.id() == id);
in_bytes || in_image || in_texture
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)
}
/// Append an entry onto the chain of bytes loaders.
/// Add a new bytes loader.
///
/// It will be tried first, before any already installed loaders.
///
/// See [`load`] for more information.
pub fn add_bytes_loader(&self, loader: Arc<dyn load::BytesLoader + Send + Sync + 'static>) {
self.loaders().bytes.lock().push(loader);
}
/// Append an entry onto the chain of image loaders.
/// Add a new image loader.
///
/// It will be tried first, before any already installed loaders.
///
/// See [`load`] for more information.
pub fn add_image_loader(&self, loader: Arc<dyn load::ImageLoader + Send + Sync + 'static>) {
self.loaders().image.lock().push(loader);
}
/// Append an entry onto the chain of texture loaders.
/// Add a new texture loader.
///
/// It will be tried first, before any already installed loaders.
///
/// See [`load`] for more information.
pub fn add_texture_loader(&self, loader: Arc<dyn load::TextureLoader + Send + Sync + 'static>) {
@ -2009,23 +2019,27 @@ impl Context {
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
/// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
///
/// ⚠ May deadlock if called from within a `BytesLoader`!
///
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
/// [custom]: crate::load::LoadError::Loading
pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult {
crate::profile_function!();
for loader in self.loaders().bytes.lock().iter() {
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::NotSupported)
Err(load::LoadError::NoMatchingBytesLoader)
}
/// Try loading the image from the given uri using any available image loaders.
@ -2041,30 +2055,31 @@ impl Context {
/// This may fail with:
/// - [`LoadError::NoImageLoaders`][no_image_loaders] if tbere are no registered image loaders.
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
/// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
///
/// ⚠ May deadlock if called from within an `ImageLoader`!
///
/// [no_image_loaders]: crate::load::LoadError::NoImageLoaders
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
/// [custom]: crate::load::LoadError::Loading
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
crate::profile_function!();
let loaders = self.loaders();
let loaders = loaders.image.lock();
if loaders.is_empty() {
let image_loaders = loaders.image.lock();
if image_loaders.is_empty() {
return Err(load::LoadError::NoImageLoaders);
}
for loader in loaders.iter() {
// 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::NotSupported)
Err(load::LoadError::NoMatchingImageLoader)
}
/// Try loading the texture from the given uri using any available texture loaders.
@ -2079,12 +2094,12 @@ impl Context {
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
/// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
///
/// ⚠ May deadlock if called from within a `TextureLoader`!
///
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
/// [custom]: crate::load::LoadError::Loading
pub fn try_load_texture(
&self,
uri: &str,
@ -2093,17 +2108,22 @@ impl Context {
) -> load::TextureLoadResult {
crate::profile_function!();
for loader in self.loaders().texture.lock().iter() {
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::NotSupported)
Err(load::LoadError::NoMatchingTextureLoader)
}
fn loaders(&self) -> Arc<Loaders> {
/// The loaders of bytes, images, and textures.
pub fn loaders(&self) -> Arc<Loaders> {
crate::profile_function!();
self.read(|this| this.loaders.clone())
}

View File

@ -437,13 +437,16 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
/// 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($path),
::std::borrow::Cow::Borrowed(concat!("bytes://", $path)), // uri
$crate::load::Bytes::Static(include_bytes!($path)),
)
};

View File

@ -3,8 +3,8 @@
//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/)
//! will get you up and running quickly with its reasonable default implementations of the traits described below.
//!
//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all-loaders` feature.
//! 2. Add a call to [`egui_extras::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`].
//!
@ -55,8 +55,6 @@
mod bytes_loader;
mod texture_loader;
use self::bytes_loader::DefaultBytesLoader;
use self::texture_loader::DefaultTextureLoader;
use crate::Context;
use ahash::HashMap;
use epaint::mutex::Mutex;
@ -69,27 +67,50 @@ use std::fmt::Debug;
use std::ops::Deref;
use std::{error::Error as StdError, fmt::Display, sync::Arc};
pub use self::bytes_loader::DefaultBytesLoader;
pub use self::texture_loader::DefaultTextureLoader;
/// Represents a failed attempt at loading an image.
#[derive(Clone, Debug)]
pub enum LoadError {
/// There are no image loaders installed.
/// Programmer error: There are no image loaders installed.
NoImageLoaders,
/// This loader does not support this protocol or image format.
/// A specific loader does not support this schema, protocol or image format.
NotSupported,
/// A custom error message (e.g. "File not found: foo.png").
Custom(String),
/// Programmer error: Failed to find the bytes for this image because
/// there was no [`BytesLoader`] supporting the schema.
NoMatchingBytesLoader,
/// Programmer error: Failed to parse the bytes as an image because
/// there was no [`ImageLoader`] supporting the schema.
NoMatchingImageLoader,
/// Programmer error: no matching [`TextureLoader`].
/// Because of the [`DefaultTextureLoader`], this error should never happen.
NoMatchingTextureLoader,
/// Runtime error: Loading was attempted, but failed (e.g. "File not found").
Loading(String),
}
impl Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LoadError::NoImageLoaders => f.write_str(
Self::NoImageLoaders => f.write_str(
"No image loaders are installed. If you're trying to load some images \
for the first time, follow the steps outlined in https://docs.rs/egui/latest/egui/load/index.html"),
LoadError::NotSupported => f.write_str("not supported"),
LoadError::Custom(message) => f.write_str(message),
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),
}
}
}
@ -238,7 +259,8 @@ pub use crate::generate_loader_id;
pub type BytesLoadResult = Result<BytesPoll>;
/// Represents a loader capable of loading raw unstructured bytes.
/// 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.
@ -261,7 +283,7 @@ pub trait BytesLoader {
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
/// - [`LoadError::Custom`] if the loading process failed.
/// - [`LoadError::Loading`] if the loading process failed.
fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult;
/// Forget the given `uri`.
@ -305,7 +327,7 @@ pub enum ImagePoll {
pub type ImageLoadResult = Result<ImagePoll>;
/// Represents a loader capable of loading a raw image.
/// An `ImageLoader` decodes raw bytes into a [`ColorImage`].
///
/// Implementations are expected to cache at least each `URI`.
pub trait ImageLoader {
@ -328,7 +350,7 @@ pub trait ImageLoader {
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
/// - [`LoadError::Custom`] if the loading process failed.
/// - [`LoadError::Loading`] if the loading process failed.
fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult;
/// Forget the given `uri`.
@ -420,7 +442,14 @@ impl TexturePoll {
pub type TextureLoadResult = Result<TexturePoll>;
/// Represents a loader capable of loading a full texture.
/// 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 {
@ -443,7 +472,7 @@ pub trait TextureLoader {
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
/// - [`LoadError::Custom`] if the loading process failed.
/// - [`LoadError::Loading`] if the loading process failed.
fn load(
&self,
ctx: &Context,
@ -479,7 +508,8 @@ type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
#[derive(Clone)]
pub(crate) struct Loaders {
/// The loaders of bytes, images, and textures.
pub struct Loaders {
pub include: Arc<DefaultBytesLoader>,
pub bytes: Mutex<Vec<BytesLoaderImpl>>,
pub image: Mutex<Vec<ImageLoaderImpl>>,

View File

@ -1,5 +1,8 @@
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>>,
@ -27,13 +30,22 @@ impl BytesLoader for 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 => Err(LoadError::NotSupported),
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)
}
}
}
}

View File

@ -207,9 +207,6 @@ pub struct Style {
///
/// This only affects a few egui widgets.
pub explanation_tooltips: bool,
/// Show a spinner when loading an image.
pub image_loading_spinners: bool,
}
impl Style {
@ -556,6 +553,9 @@ pub struct Visuals {
/// 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 {
@ -741,7 +741,6 @@ impl Default for Style {
animation_time: 1.0 / 12.0,
debug: Default::default(),
explanation_tooltips: false,
image_loading_spinners: true,
}
}
}
@ -821,6 +820,8 @@ impl Visuals {
slider_trailing_fill: false,
interact_cursor: None,
image_loading_spinners: true,
}
}
@ -994,7 +995,6 @@ impl Style {
animation_time,
debug,
explanation_tooltips,
image_loading_spinners,
} = self;
visuals.light_dark_radio_buttons(ui);
@ -1062,9 +1062,6 @@ impl Style {
"Show explanatory text when hovering DragValue:s and other egui widgets",
);
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));
}
}
@ -1396,6 +1393,8 @@ impl Visuals {
slider_trailing_fill,
interact_cursor,
image_loading_spinners,
} = self;
ui.collapsing("Background Colors", |ui| {
@ -1471,6 +1470,9 @@ impl Visuals {
}
});
ui.checkbox(image_loading_spinners, "Image loading spinners")
.on_hover_text("Show a spinner when an Image is loading");
ui.vertical_centered(|ui| reset_button(ui, self));
}
}

View File

@ -1561,7 +1561,7 @@ impl Ui {
/// Show an image available at the given `uri`.
///
/// ⚠ This will do nothing unless you install some image loaders first!
/// The easiest way to do this is via [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
/// The easiest way to do this is via [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
///
/// The loaders handle caching image data, sampled textures, etc. across frames, so calling this is immediate-mode safe.
///
@ -1577,9 +1577,10 @@ impl Ui {
/// # });
/// ```
///
/// Note: Prefer `include_image` as a source if you're loading an image
/// from a file with a statically known path, unless you really want to
/// load it at runtime instead!
/// 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]
@ -1687,7 +1688,7 @@ impl Ui {
/// # });
/// ```
///
/// Se also [`Self::scope`].
/// See also [`Self::scope`].
pub fn group<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
crate::Frame::group(self.style()).show(self, add_contents)
}

View File

@ -1,5 +1,3 @@
use crate::load::SizedTexture;
use crate::load::TexturePoll;
use crate::*;
/// Clickable button with text.
@ -22,7 +20,8 @@ use crate::*;
/// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Button<'a> {
text: WidgetText,
image: Option<Image<'a>>,
text: Option<WidgetText>,
shortcut_text: WidgetText,
wrap: Option<bool>,
@ -34,13 +33,30 @@ pub struct Button<'a> {
frame: Option<bool>,
min_size: Vec2,
rounding: Option<Rounding>,
image: Option<Image<'a>>,
selected: bool,
}
impl<'a> Button<'a> {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self::opt_image_and_text(None, Some(text.into()))
}
/// Creates a button with an image. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)]
pub fn image(image: impl Into<Image<'a>>) -> Self {
Self::opt_image_and_text(Some(image.into()), None)
}
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)]
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
Self::opt_image_and_text(Some(image.into()), Some(text.into()))
}
pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
Self {
text: text.into(),
text,
image,
shortcut_text: Default::default(),
wrap: None,
fill: None,
@ -50,16 +66,7 @@ impl<'a> Button<'a> {
frame: None,
min_size: Vec2::ZERO,
rounding: None,
image: None,
}
}
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
#[allow(clippy::needless_pass_by_value)]
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
Self {
image: Some(image.into()),
..Self::new(text)
selected: false,
}
}
@ -94,7 +101,9 @@ impl<'a> Button<'a> {
/// Make this a small button, suitable for embedding into text.
pub fn small(mut self) -> Self {
self.text = self.text.text_style(TextStyle::Body);
if let Some(text) = self.text {
self.text = Some(text.text_style(TextStyle::Body));
}
self.small = true;
self
}
@ -133,12 +142,19 @@ impl<'a> Button<'a> {
self.shortcut_text = shortcut_text.into();
self
}
/// If `true`, mark this button as "selected".
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl Widget for Button<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let Button {
text,
image,
shortcut_text,
wrap,
fill,
@ -148,21 +164,35 @@ impl Widget for Button<'_> {
frame,
min_size,
rounding,
image,
selected,
} = self;
let image_size = image
.as_ref()
.and_then(|image| image.load_and_calculate_size(ui, ui.available_size()))
.unwrap_or(Vec2::ZERO);
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
let mut button_padding = ui.spacing().button_padding;
let mut button_padding = if frame {
ui.spacing().button_padding
} else {
Vec2::ZERO
};
if small {
button_padding.y = 0.0;
}
let space_available_for_image = if let Some(text) = &text {
let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style()));
Vec2::splat(font_height) // Reasonable?
} else {
ui.available_size() - 2.0 * button_padding
};
let image_size = if let Some(image) = &image {
image
.load_and_calculate_size(ui, space_available_for_image)
.unwrap_or(space_available_for_image)
} else {
Vec2::ZERO
};
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
if image.is_some() {
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
@ -171,15 +201,22 @@ impl Widget for Button<'_> {
text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap).
}
let text = text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button);
let text = text.map(|text| text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button));
let shortcut_text = (!shortcut_text.is_empty())
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
let mut desired_size = text.size();
let mut desired_size = Vec2::ZERO;
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);
}
if image.is_some() && text.is_some() {
desired_size.x += ui.spacing().icon_spacing;
}
if let Some(text) = &text {
desired_size.x += text.size().x;
desired_size.y = desired_size.y.max(text.size().y);
}
if let Some(shortcut_text) = &shortcut_text {
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
desired_size.y = desired_size.y.max(shortcut_text.size().y);
@ -191,31 +228,81 @@ impl Widget for Button<'_> {
desired_size = desired_size.at_least(min_size);
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) {
let visuals = ui.style().interact(&response);
if frame {
let fill = fill.unwrap_or(visuals.weak_bg_fill);
let stroke = stroke.unwrap_or(visuals.bg_stroke);
let rounding = rounding.unwrap_or(visuals.rounding);
ui.painter()
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
}
let text_pos = if image.is_some() {
let icon_spacing = ui.spacing().icon_spacing;
pos2(
rect.min.x + button_padding.x + image_size.x + icon_spacing,
rect.center().y - 0.5 * text.size().y,
let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected {
let selection = ui.visuals().selection;
(
Vec2::ZERO,
Rounding::ZERO,
selection.bg_fill,
selection.stroke,
)
} else if frame {
let expansion = Vec2::splat(visuals.expansion);
(
expansion,
visuals.rounding,
visuals.weak_bg_fill,
visuals.bg_stroke,
)
} else {
ui.layout()
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min
Default::default()
};
text.paint_with_visuals(ui.painter(), text_pos, visuals);
let frame_rounding = rounding.unwrap_or(frame_rounding);
let frame_fill = fill.unwrap_or(frame_fill);
let frame_stroke = stroke.unwrap_or(frame_stroke);
ui.painter().rect(
rect.expand2(frame_expansion),
frame_rounding,
frame_fill,
frame_stroke,
);
let mut cursor_x = rect.min.x + button_padding.x;
if let Some(image) = &image {
let image_rect = Rect::from_min_size(
pos2(cursor_x, rect.center().y - 0.5 - (image_size.y / 2.0)),
image_size,
);
cursor_x += image_size.x;
let tlr = image.load(ui);
widgets::image::paint_texture_load_result(
ui,
&tlr,
image_rect,
image.show_loading_spinner,
image.image_options(),
);
response =
widgets::image::texture_load_result_response(image.source(), &tlr, response);
}
if image.is_some() && text.is_some() {
cursor_x += ui.spacing().icon_spacing;
}
if let Some(text) = text {
let text_pos = if image.is_some() || shortcut_text.is_some() {
pos2(cursor_x, rect.center().y - 0.5 * text.size().y)
} else {
// Make sure button text is centered if within a centered layout
ui.layout()
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min
};
text.paint_with_visuals(ui.painter(), text_pos, visuals);
}
if let Some(shortcut_text) = shortcut_text {
let shortcut_text_pos = pos2(
@ -228,29 +315,6 @@ impl Widget for Button<'_> {
ui.visuals().weak_text_color(),
);
}
if let Some(image) = &image {
let image_rect = Rect::from_min_size(
pos2(
rect.min.x + button_padding.x,
rect.center().y - 0.5 - (image_size.y / 2.0),
),
image_size,
);
let tlr = image.load(ui);
let show_loading_spinner = image
.show_loading_spinner
.unwrap_or(ui.style().image_loading_spinners);
widgets::image::paint_texture_load_result(
ui,
&tlr,
image_rect,
show_loading_spinner,
image.image_options(),
);
response =
widgets::image::texture_load_result_response(image.source(), &tlr, response);
}
}
if let Some(cursor) = ui.visuals().interact_cursor {
@ -530,8 +594,10 @@ impl<'a> ImageButton<'a> {
self.sense = sense;
self
}
}
fn show(&self, ui: &mut Ui, texture: &SizedTexture) -> Response {
impl<'a> Widget for ImageButton<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let padding = if self.frame {
// so we can see that it is a button:
Vec2::splat(ui.spacing().button_padding.x)
@ -539,7 +605,13 @@ impl<'a> ImageButton<'a> {
Vec2::ZERO
};
let padded_size = texture.size + 2.0 * padding;
let tlr = self.image.load(ui);
let texture_size = tlr.as_ref().ok().and_then(|t| t.size());
let image_size = self
.image
.calculate_size(ui.available_size() - 2.0 * padding, texture_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));
@ -571,36 +643,19 @@ impl<'a> ImageButton<'a> {
let image_rect = ui
.layout()
.align_size_within_rect(texture.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_options = ImageOptions {
rounding, // apply rounding to the image
..self.image.image_options().clone()
};
crate::widgets::image::paint_image_at(ui, image_rect, &image_options, texture);
widgets::image::paint_texture_load_result(ui, &tlr, image_rect, None, &image_options);
// Draw frame outline:
ui.painter()
.rect_stroke(rect.expand2(expansion), rounding, stroke);
}
response
}
}
impl<'a> Widget for ImageButton<'a> {
fn ui(self, ui: &mut Ui) -> Response {
match self.image.load(ui) {
Ok(TexturePoll::Ready { mut texture }) => {
texture.size = self.image.calculate_size(ui.available_size(), texture.size);
self.show(ui, &texture)
}
Ok(TexturePoll::Pending { .. }) => ui
.spinner()
.on_hover_text(format!("Loading {:?}", self.image.uri())),
Err(err) => ui
.colored_label(ui.visuals().error_fg_color, "")
.on_hover_text(err.to_string()),
}
widgets::image::texture_load_result_response(self.image.source(), &tlr, response)
}
}

View File

@ -74,6 +74,8 @@ impl<'a> Image<'a> {
/// Load the image from some raw bytes.
///
/// For better error messages, use the `bytes://` prefix for the URI.
///
/// See [`ImageSource::Bytes`].
pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self {
Self::new(ImageSource::Bytes(uri.into(), bytes.into()))
@ -220,7 +222,7 @@ impl<'a> Image<'a> {
/// Show a spinner when the image is loading.
///
/// By default this uses the value of [`Style::image_loading_spinners`].
/// By default this uses the value of [`Visuals::image_loading_spinners`].
#[inline]
pub fn show_loading_spinner(mut self, show: bool) -> Self {
self.show_loading_spinner = Some(show);
@ -237,7 +239,8 @@ impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
impl<'a> Image<'a> {
/// Returns the size the image will occupy in the final UI.
#[inline]
pub fn calculate_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
pub fn calculate_size(&self, available_size: Vec2, image_size: Option<Vec2>) -> Vec2 {
let image_size = image_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load.
self.size.get(available_size, image_size)
}
@ -264,18 +267,9 @@ impl<'a> Image<'a> {
&self.source
}
/// Get the `uri` that this image was constructed from.
///
/// This will return `<unknown>` for [`ImageSource::Texture`].
#[inline]
pub fn uri(&self) -> &str {
self.source.uri().unwrap_or("<unknown>")
}
/// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
///
/// # Errors
///
/// May fail if they underlying [`Context::try_load_texture`] call fails.
pub fn load(&self, ui: &Ui) -> TextureLoadResult {
let size_hint = self.size.hint(ui.available_size());
@ -284,41 +278,34 @@ impl<'a> Image<'a> {
.load(ui.ctx(), self.texture_options, size_hint)
}
/// Paint the image in the given rectangle.
#[inline]
pub fn paint_at(&self, ui: &mut Ui, rect: Rect, texture: &SizedTexture) {
paint_image_at(ui, rect, &self.image_options, texture);
pub fn paint_at(&self, ui: &mut Ui, rect: Rect) {
paint_texture_load_result(
ui,
&self.load(ui),
rect,
self.show_loading_spinner,
&self.image_options,
);
}
}
impl<'a> Widget for Image<'a> {
fn ui(self, ui: &mut Ui) -> Response {
match self.load(ui) {
Ok(texture_poll) => {
let texture_size = texture_poll.size();
let texture_size =
texture_size.unwrap_or_else(|| Vec2::splat(ui.style().spacing.interact_size.y));
let ui_size = self.calculate_size(ui.available_size(), texture_size);
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
match texture_poll {
TexturePoll::Ready { texture } => {
self.paint_at(ui, rect, &texture);
response
}
TexturePoll::Pending { .. } => {
let show_spinner = self
.show_loading_spinner
.unwrap_or(ui.style().image_loading_spinners);
if show_spinner {
Spinner::new().paint_at(ui, response.rect);
}
response.on_hover_text(format!("Loading {:?}", self.uri()))
}
}
}
Err(err) => ui
.colored_label(ui.visuals().error_fg_color, "")
.on_hover_text(err.to_string()),
}
let tlr = self.load(ui);
let texture_size = tlr.as_ref().ok().and_then(|t| t.size());
let ui_size = self.calculate_size(ui.available_size(), texture_size);
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
paint_texture_load_result(
ui,
&tlr,
rect,
self.show_loading_spinner,
&self.image_options,
);
texture_load_result_response(&self.source, &tlr, response)
}
}
@ -353,12 +340,16 @@ pub struct ImageSize {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageFit {
/// Fit the image to its original size, scaled by some factor.
///
/// Ignores how much space is actually available in the ui.
Original { scale: f32 },
/// Fit the image to a fraction of the available size.
Fraction(Vec2),
/// Fit the image to an exact size.
///
/// Ignores how much space is actually available in the ui.
Exact(Vec2),
}
@ -373,7 +364,7 @@ impl ImageFit {
}
impl ImageSize {
fn hint(&self, available_size: Vec2) -> SizeHint {
pub fn hint(&self, available_size: Vec2) -> SizeHint {
if self.maintain_aspect_ratio {
return SizeHint::Scale(1.0.ord());
};
@ -395,7 +386,7 @@ impl ImageSize {
}
}
fn get(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
pub fn get(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
let Self {
maintain_aspect_ratio,
max_size,
@ -449,7 +440,7 @@ impl Default for ImageSize {
/// This type tells the [`Ui`] how to load an image.
///
/// This is used by [`Image::new`] and [`Ui::image`].
#[derive(Debug, Clone)]
#[derive(Clone)]
pub enum ImageSource<'a> {
/// Load the image from a URI.
///
@ -468,6 +459,8 @@ pub enum ImageSource<'a> {
/// Load the image from some raw bytes.
///
/// For better error messages, use the `bytes://` prefix for the URI.
///
/// The [`Bytes`] may be:
/// - `'static`, obtained from `include_bytes!` or similar
/// - Anything that can be converted to `Arc<[u8]>`
@ -480,6 +473,15 @@ pub enum ImageSource<'a> {
Bytes(Cow<'static, str>, Bytes),
}
impl<'a> std::fmt::Debug for ImageSource<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageSource::Bytes(uri, _) | ImageSource::Uri(uri) => uri.as_ref().fmt(f),
ImageSource::Texture(st) => st.id.fmt(f),
}
}
}
impl<'a> ImageSource<'a> {
/// # Errors
/// Failure to load the texture.
@ -514,7 +516,7 @@ pub fn paint_texture_load_result(
ui: &Ui,
tlr: &TextureLoadResult,
rect: Rect,
show_loading_spinner: bool,
show_loading_spinner: Option<bool>,
options: &ImageOptions,
) {
match tlr {
@ -522,6 +524,8 @@ pub fn paint_texture_load_result(
paint_image_at(ui, rect, options, texture);
}
Ok(TexturePoll::Pending { .. }) => {
let show_loading_spinner =
show_loading_spinner.unwrap_or(ui.visuals().image_loading_spinners);
if show_loading_spinner {
Spinner::new().paint_at(ui, rect);
}
@ -539,6 +543,7 @@ pub fn paint_texture_load_result(
}
}
/// Attach tooltips like "Loading…" or "Failed loading: …".
pub fn texture_load_result_response(
source: &ImageSource<'_>,
tlr: &TextureLoadResult,
@ -547,13 +552,13 @@ pub fn texture_load_result_response(
match tlr {
Ok(TexturePoll::Ready { .. }) => response,
Ok(TexturePoll::Pending { .. }) => {
if let Some(uri) = source.uri() {
response.on_hover_text(format!("Loading {uri}"))
} else {
response.on_hover_text("Loading image…")
}
let uri = source.uri().unwrap_or("image");
response.on_hover_text(format!("Loading {uri}"))
}
Err(err) => {
let uri = source.uri().unwrap_or("image");
response.on_hover_text(format!("Failed loading {uri}: {err}"))
}
Err(err) => response.on_hover_text(err.to_string()),
}
}

View File

@ -32,6 +32,7 @@ impl Spinner {
self
}
/// Paint the spinner in the given rectangle.
pub fn paint_at(&self, ui: &Ui, rect: Rect) {
if ui.is_rect_visible(rect) {
ui.ctx().request_repaint();

View File

@ -19,7 +19,7 @@ crate-type = ["cdylib", "rlib"]
default = ["glow", "persistence"]
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
image_viewer = ["image", "egui_extras/all-loaders", "rfd"]
image_viewer = ["image", "egui_extras/all_loaders", "rfd"]
persistence = ["eframe/persistence", "egui/persistence", "serde"]
web_screen_reader = ["eframe/web_screen_reader"] # experimental
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]

View File

@ -165,7 +165,7 @@ pub struct WrapApp {
impl WrapApp {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
egui_extras::loaders::install(&_cc.egui_ctx);
egui_extras::install_image_loaders(&_cc.egui_ctx);
#[allow(unused_mut)]
let mut slf = Self {

View File

@ -201,11 +201,12 @@ impl WidgetGallery {
ui.add(egui::Image::new(egui_icon.clone()));
ui.end_row();
ui.add(doc_link_label("ImageButton", "ImageButton"));
ui.add(doc_link_label(
"Button with image",
"Button::image_and_text",
));
if ui
.add(egui::ImageButton::new(
egui::Image::from(egui_icon).max_size(egui::Vec2::splat(16.0)),
))
.add(egui::Button::image_and_text(egui_icon, "Click me!"))
.clicked()
{
*boolean = !*boolean;

View File

@ -27,7 +27,7 @@ all-features = true
default = ["dep:mime_guess"]
## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`).
all-loaders = ["file", "http", "image", "svg"]
all_loaders = ["file", "http", "image", "svg"]
## Enable [`DatePickerButton`] widget.
datepicker = ["chrono"]
@ -38,6 +38,14 @@ file = ["dep:mime_guess"]
## Add support for loading images via HTTP.
http = ["dep:ehttp"]
## Add support for loading images with the [`image`](https://docs.rs/image) crate.
##
## You also need to ALSO opt-in to the image formats you want to support, like so:
## ```toml
## image = { version = "0.24", features = ["jpeg", "png"] }
## ```
image = ["dep:image"]
## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers.
@ -71,12 +79,6 @@ chrono = { version = "0.4", optional = true, default-features = false, features
## Enable this when generating docs.
document-features = { version = "0.2", optional = true }
## Add support for loading images with the [`image`](https://docs.rs/image) crate.
##
## You also need to ALSO opt-in to the image formats you want to support, like so:
## ```toml
## image = { version = "0.24", features = ["jpeg", "png"] }
## ```
image = { version = "0.24", optional = true, default-features = false }
# file feature

View File

@ -34,6 +34,8 @@ pub use crate::sizing::Size;
pub use crate::strip::*;
pub use crate::table::*;
pub use loaders::install_image_loaders;
// ---------------------------------------------------------------------------
mod profiling_scopes {

View File

@ -1,22 +1,25 @@
// 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 `http` feature)
/// - `image` loader (with the `image` feature)
/// - `svg` loader with the `svg` feature
/// Calling this enables the use of [`egui::Image`] and [`egui::Ui::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.
///
/// ⚠ This will do nothing and you won't see any images unless you enable some features:
///
/// - If you just want to be able to load `file://` and `http://` URIs, enable the `all-loaders` feature.
/// - If you just want to be able to load `file://` and `http://` URIs, enable the `all_loaders` feature.
/// - The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image)
/// crate as your direct dependency, and enabling features on it:
///
/// ```toml,ignore
/// egui_extras = { version = "*", features = ["all_loaders"] }
/// image = { version = "0.24", features = ["jpeg", "png"] }
/// ```
///
@ -39,7 +42,8 @@
/// 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 load any URI without an extension.
/// 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`],
@ -51,7 +55,7 @@
/// and must include `svg` for it to be considered supported. For example, `image/svg+xml` would be loaded by the `svg` loader.
///
/// See [`egui::load`] for more information about how loaders work.
pub fn install(ctx: &egui::Context) {
pub fn install_image_loaders(ctx: &egui::Context) {
#[cfg(all(not(target_arch = "wasm32"), feature = "file"))]
if !ctx.is_loader_installed(self::file_loader::FileLoader::ID) {
ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default()));
@ -86,7 +90,7 @@ pub fn install(ctx: &egui::Context) {
not(feature = "image"),
not(feature = "svg")
))]
log::warn!("`loaders::install` was called, but no loaders are enabled");
log::warn!("`install_image_loaders` was called, but no loaders are enabled");
let _ = ctx;
}

View File

@ -72,7 +72,7 @@ impl BytesLoader for EhttpLoader {
bytes: Bytes::Shared(file.bytes),
mime: file.mime,
}),
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
Poll::Ready(Err(err)) => Err(LoadError::Loading(err)),
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
}
} else {

View File

@ -45,7 +45,7 @@ impl BytesLoader for FileLoader {
bytes: Bytes::Shared(file.bytes),
mime: file.mime,
}),
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
Poll::Ready(Err(err)) => Err(LoadError::Loading(err)),
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
}
} else {

View File

@ -50,7 +50,7 @@ impl ImageLoader for ImageCrateLoader {
if let Some(entry) = cache.get(uri).cloned() {
match entry {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)),
Err(err) => Err(LoadError::Loading(err)),
}
} else {
match ctx.try_load_bytes(uri) {
@ -68,7 +68,7 @@ impl ImageLoader for ImageCrateLoader {
cache.insert(uri.into(), result.clone());
match result {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)),
Err(err) => Err(LoadError::Loading(err)),
}
}
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),

View File

@ -42,7 +42,7 @@ impl ImageLoader for SvgLoader {
if let Some(entry) = cache.get(&(uri.clone(), size_hint)).cloned() {
match entry {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)),
Err(err) => Err(LoadError::Loading(err)),
}
} else {
match ctx.try_load_bytes(&uri) {
@ -60,7 +60,7 @@ impl ImageLoader for SvgLoader {
cache.insert((uri, size_hint), result.clone());
match result {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)),
Err(err) => Err(LoadError::Loading(err)),
}
}
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),

View File

@ -370,25 +370,6 @@ impl Painter {
Primitive::Callback(callback) => {
if callback.rect.is_positive() {
crate::profile_scope!("callback");
// Transform callback rect to physical pixels:
let rect_min_x = pixels_per_point * callback.rect.min.x;
let rect_min_y = pixels_per_point * callback.rect.min.y;
let rect_max_x = pixels_per_point * callback.rect.max.x;
let rect_max_y = pixels_per_point * callback.rect.max.y;
let rect_min_x = rect_min_x.round() as i32;
let rect_min_y = rect_min_y.round() as i32;
let rect_max_x = rect_max_x.round() as i32;
let rect_max_y = rect_max_y.round() as i32;
unsafe {
self.gl.viewport(
rect_min_x,
size_in_pixels.1 as i32 - rect_max_y,
rect_max_x - rect_min_x,
rect_max_y - rect_min_y,
);
}
let info = egui::PaintCallbackInfo {
viewport: callback.rect,
@ -397,6 +378,16 @@ impl Painter {
screen_size_px,
};
let viewport_px = info.viewport_in_pixels();
unsafe {
self.gl.viewport(
viewport_px.left_px.round() as _,
viewport_px.from_bottom_px.round() as _,
viewport_px.width_px.round() as _,
viewport_px.height_px.round() as _,
);
}
if let Some(callback) = callback.callback.downcast_ref::<CallbackFn>() {
(callback.f)(info, self);
} else {

View File

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

View File

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

View File

@ -12,7 +12,7 @@ publish = false
eframe = { path = "../../crates/eframe", features = [
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
egui_extras = { path = "../../crates/egui_extras", features = ["all-loaders"] }
egui_extras = { path = "../../crates/egui_extras", features = ["all_loaders"] }
env_logger = "0.10"
image = { version = "0.24", default-features = false, features = [
"jpeg",

View File

@ -1,12 +1,11 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
use eframe::epaint::vec2;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
drag_and_drop_support: true,
initial_window_size: Some(egui::vec2(600.0, 800.0)),
..Default::default()
};
eframe::run_native(
@ -14,7 +13,7 @@ fn main() -> Result<(), eframe::Error> {
options,
Box::new(|cc| {
// The following call is needed to load images when using `ui.image` and `egui::Image`:
egui_extras::loaders::install(&cc.egui_ctx);
egui_extras::install_image_loaders(&cc.egui_ctx);
Box::<MyApp>::default()
}),
)
@ -27,10 +26,8 @@ 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.add(
egui::Image::new(egui::include_image!("ferris.svg"))
.fit_to_fraction(vec2(1.0, 0.5)),
);
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)),