1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 06:40:06 -04:00

Track original SVG size (#7098)

This fixes bugs related to how an `Image` follows the size of an SVG.

We track the "source size" of each image, i.e. the original width/height
of the SVG, which can be different from whatever it was rasterized as.
This commit is contained in:
Emil Ernerfeldt
2025-05-28 08:33:01 +02:00
committed by GitHub
parent da67465a6c
commit 2cf6a3a9a6
12 changed files with 180 additions and 96 deletions

View File

@@ -143,12 +143,16 @@ pub type Result<T, E = LoadError> = std::result::Result<T, E>;
/// Given as a hint for image loading requests.
///
/// Used mostly for rendering SVG:s to a good size.
/// The size is measured in texels, with the pixels per point already factored in.
///
/// All variants will preserve the original aspect ratio.
/// The [`SizeHint`] determines at what resolution the image should be rasterized.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SizeHint {
/// Scale original size by some factor.
/// Scale original size by some factor, keeping the original aspect ratio.
///
/// The original size of the image is usually its texel resolution,
/// but for an SVG it's the point size of the SVG.
///
/// For instance, setting `Scale(2.0)` will rasterize SVG:s to twice their original size,
/// which is useful for high-DPI displays.
Scale(OrderedFloat<f32>),
/// Scale to exactly this pixel width, keeping the original aspect ratio.
@@ -168,6 +172,26 @@ pub enum SizeHint {
},
}
impl SizeHint {
/// Multiply size hint by a factor.
pub fn scale_by(self, factor: f32) -> Self {
match self {
Self::Scale(scale) => Self::Scale(OrderedFloat(factor * scale.0)),
Self::Width(width) => Self::Width((factor * width as f32).round() as _),
Self::Height(height) => Self::Height((factor * height as f32).round() as _),
Self::Size {
width,
height,
maintain_aspect_ratio,
} => Self::Size {
width: (factor * width as f32).round() as _,
height: (factor * height as f32).round() as _,
maintain_aspect_ratio,
},
}
}
}
impl Default for SizeHint {
#[inline]
fn default() -> Self {
@@ -249,12 +273,16 @@ impl Deref for Bytes {
pub enum BytesPoll {
/// Bytes are being loaded.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
/// Bytes are loaded.
Ready {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
@@ -344,6 +372,8 @@ pub trait BytesLoader {
pub enum ImagePoll {
/// Image is loading.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
@@ -414,7 +444,7 @@ pub trait ImageLoader {
pub struct SizedTexture {
pub id: TextureId,
/// Size in logical ui points.
/// Point size of the original SVG, or the size of the image in texels.
pub size: Vec2,
}
@@ -460,6 +490,8 @@ impl<'a> From<&'a TextureHandle> for SizedTexture {
pub enum TexturePoll {
/// Texture is loading.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
@@ -469,6 +501,7 @@ pub enum TexturePoll {
}
impl TexturePoll {
/// Point size of the original SVG, or the size of the image in texels.
#[inline]
pub fn size(&self) -> Option<Vec2> {
match self {

View File

@@ -1,5 +1,7 @@
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
use emath::Vec2;
use super::{
BytesLoader as _, Context, HashMap, ImagePoll, Mutex, SizeHint, SizedTexture, TextureHandle,
TextureLoadResult, TextureLoader, TextureOptions, TexturePoll,
@@ -16,6 +18,10 @@ type Bucket = HashMap<Option<SizeHint>, Entry>;
struct Entry {
last_used: AtomicU64,
/// Size of the original SVG, if any, or the texel size of the image if not an SVG.
source_size: Vec2,
handle: TextureHandle,
}
@@ -61,18 +67,20 @@ impl TextureLoader for DefaultTextureLoader {
texture
.last_used
.store(self.pass_index.load(Relaxed), Relaxed);
let texture = SizedTexture::from_handle(&texture.handle);
let texture = SizedTexture::new(texture.handle.id(), texture.source_size);
Ok(TexturePoll::Ready { texture })
} else {
match ctx.try_load_image(uri, size_hint)? {
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
ImagePoll::Ready { image } => {
let source_size = image.source_size;
let handle = ctx.load_texture(uri, image, texture_options);
let texture = SizedTexture::from_handle(&handle);
let texture = SizedTexture::new(handle.id(), source_size);
bucket.insert(
svg_size_hint,
Entry {
last_used: AtomicU64::new(self.pass_index.load(Relaxed)),
source_size,
handle,
},
);

View File

@@ -156,6 +156,9 @@ impl<'a> Image<'a> {
/// Fit the image to its original size with some scaling.
///
/// The texel size of the source image will be multiplied by the `scale` factor,
/// and then become the _ui_ size of the [`Image`].
///
/// This will cause the image to overflow if it is larger than the available space.
///
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
@@ -291,9 +294,9 @@ 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 calc_size(&self, available_size: Vec2, original_image_size: Option<Vec2>) -> Vec2 {
let original_image_size = original_image_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load.
self.size.calc_size(available_size, original_image_size)
pub fn calc_size(&self, available_size: Vec2, image_source_size: Option<Vec2>) -> Vec2 {
let image_source_size = image_source_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load.
self.size.calc_size(available_size, image_source_size)
}
pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option<Vec2> {
@@ -405,8 +408,8 @@ impl<'a> Image<'a> {
impl Widget for Image<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let tlr = self.load_for_size(ui.ctx(), ui.available_size());
let original_image_size = tlr.as_ref().ok().and_then(|t| t.size());
let ui_size = self.calc_size(ui.available_size(), original_image_size);
let image_source_size = tlr.as_ref().ok().and_then(|t| t.size());
let ui_size = self.calc_size(ui.available_size(), image_source_size);
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
response.widget_info(|| {
@@ -458,7 +461,10 @@ pub struct ImageSize {
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageFit {
/// Fit the image to its original size, scaled by some factor.
/// Fit the image to its original srce size, scaled by some factor.
///
/// The original size of the image is usually its texel resolution,
/// but for an SVG it's the point size of the SVG.
///
/// Ignores how much space is actually available in the ui.
Original { scale: f32 },
@@ -516,7 +522,7 @@ impl ImageSize {
}
/// Calculate the final on-screen size in points.
pub fn calc_size(&self, available_size: Vec2, original_image_size: Vec2) -> Vec2 {
pub fn calc_size(&self, available_size: Vec2, image_source_size: Vec2) -> Vec2 {
let Self {
maintain_aspect_ratio,
max_size,
@@ -524,7 +530,7 @@ impl ImageSize {
} = *self;
match fit {
ImageFit::Original { scale } => {
let image_size = original_image_size * scale;
let image_size = scale * image_source_size;
if image_size.x <= max_size.x && image_size.y <= max_size.y {
image_size
} else {
@@ -533,11 +539,11 @@ impl ImageSize {
}
ImageFit::Fraction(fract) => {
let scale_to_size = (available_size * fract).min(max_size);
scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio)
scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio)
}
ImageFit::Exact(size) => {
let scale_to_size = size.min(max_size);
scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio)
scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio)
}
}
}

View File

@@ -93,10 +93,10 @@ impl Widget for ImageButton<'_> {
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_source_size = tlr.as_ref().ok().and_then(|t| t.size());
let image_size = self
.image
.calc_size(available_size_for_image, original_image_size);
.calc_size(available_size_for_image, image_source_size);
let padded_size = image_size + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);