mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 21:30:03 -04:00
Merge branch 'lucas/atoms-preferred-size' into lucas/experiments/measure-widget-size
# Conflicts: # crates/egui/src/ui.rs # crates/egui/src/widgets/button.rs # crates/egui/src/widgets/label.rs # crates/egui_demo_lib/src/demo/popups.rs # crates/egui_extras/src/layout.rs # crates/epaint/src/text/text_layout_types.rs
This commit is contained in:
@@ -2,7 +2,7 @@ mod button;
|
||||
mod popup;
|
||||
|
||||
pub use button::DatePickerButton;
|
||||
use chrono::{Datelike, Duration, NaiveDate, Weekday};
|
||||
use chrono::{Datelike as _, Duration, NaiveDate, Weekday};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Week {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::{Datelike, NaiveDate, Weekday};
|
||||
use chrono::{Datelike as _, NaiveDate, Weekday};
|
||||
|
||||
use egui::{Align, Button, Color32, ComboBox, Direction, Id, Layout, RichText, Ui, Vec2};
|
||||
|
||||
|
||||
@@ -1,193 +1,6 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use egui::{mutex::Mutex, TextureOptions};
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
use egui::SizeHint;
|
||||
|
||||
/// An image to be shown in egui.
|
||||
///
|
||||
/// Load once, and save somewhere in your app state.
|
||||
///
|
||||
/// Use the `svg` and `image` features to enable more constructors.
|
||||
///
|
||||
/// ⚠ This type is deprecated: Consider using [`egui::Image`] instead.
|
||||
#[deprecated = "consider using `egui::Image` instead"]
|
||||
pub struct RetainedImage {
|
||||
debug_name: String,
|
||||
|
||||
size: [usize; 2],
|
||||
|
||||
/// Cleared once [`Self::texture`] has been loaded.
|
||||
image: Mutex<egui::ColorImage>,
|
||||
|
||||
/// Lazily loaded when we have an egui context.
|
||||
texture: Mutex<Option<egui::TextureHandle>>,
|
||||
|
||||
options: TextureOptions,
|
||||
}
|
||||
|
||||
impl RetainedImage {
|
||||
pub fn from_color_image(debug_name: impl Into<String>, image: egui::ColorImage) -> Self {
|
||||
Self {
|
||||
debug_name: debug_name.into(),
|
||||
size: image.size,
|
||||
image: Mutex::new(image),
|
||||
texture: Default::default(),
|
||||
options: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a (non-svg) image.
|
||||
///
|
||||
/// `image_bytes` should be the raw contents of an image file (`.png`, `.jpg`, …).
|
||||
///
|
||||
/// Requires the "image" feature. You must also opt-in to the image formats you need
|
||||
/// with e.g. `image = { version = "0.25", features = ["jpeg", "png"] }`.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image or unsupported image format.
|
||||
#[cfg(feature = "image")]
|
||||
pub fn from_image_bytes(
|
||||
debug_name: impl Into<String>,
|
||||
image_bytes: &[u8],
|
||||
) -> Result<Self, String> {
|
||||
Ok(Self::from_color_image(
|
||||
debug_name,
|
||||
load_image_bytes(image_bytes).map_err(|err| err.to_string())?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Pass in the bytes of an SVG that you've loaded.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn from_svg_bytes(debug_name: impl Into<String>, svg_bytes: &[u8]) -> Result<Self, String> {
|
||||
Self::from_svg_bytes_with_size(debug_name, svg_bytes, None)
|
||||
}
|
||||
|
||||
/// Pass in the str of an SVG that you've loaded.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn from_svg_str(debug_name: impl Into<String>, svg_str: &str) -> Result<Self, String> {
|
||||
Self::from_svg_bytes(debug_name, svg_str.as_bytes())
|
||||
}
|
||||
|
||||
/// Pass in the bytes of an SVG that you've loaded
|
||||
/// and the scaling option to resize the SVG with
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn from_svg_bytes_with_size(
|
||||
debug_name: impl Into<String>,
|
||||
svg_bytes: &[u8],
|
||||
size_hint: Option<SizeHint>,
|
||||
) -> Result<Self, String> {
|
||||
Ok(Self::from_color_image(
|
||||
debug_name,
|
||||
load_svg_bytes_with_size(svg_bytes, size_hint)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Set the texture filters to use for the image.
|
||||
///
|
||||
/// **Note:** If the texture has already been uploaded to the GPU, this will require
|
||||
/// re-uploading the texture with the updated filter.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use egui_extras::RetainedImage;
|
||||
/// # use egui::{Color32, epaint::{ColorImage, textures::TextureOptions}};
|
||||
/// # let pixels = vec![Color32::BLACK];
|
||||
/// # let color_image = ColorImage {
|
||||
/// # size: [1, 1],
|
||||
/// # pixels,
|
||||
/// # };
|
||||
/// #
|
||||
/// // Upload a pixel art image without it getting blurry when resized
|
||||
/// let image = RetainedImage::from_color_image("my_image", color_image)
|
||||
/// .with_options(TextureOptions::NEAREST);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn with_options(mut self, options: TextureOptions) -> Self {
|
||||
self.options = options;
|
||||
|
||||
// If the texture has already been uploaded, this will force it to be re-uploaded with the
|
||||
// updated filter.
|
||||
*self.texture.lock() = None;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// The size of the image data (number of pixels wide/high).
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// The width of the image.
|
||||
pub fn width(&self) -> usize {
|
||||
self.size[0]
|
||||
}
|
||||
|
||||
/// The height of the image.
|
||||
pub fn height(&self) -> usize {
|
||||
self.size[1]
|
||||
}
|
||||
|
||||
/// The size of the image data (number of pixels wide/high).
|
||||
pub fn size_vec2(&self) -> egui::Vec2 {
|
||||
let [w, h] = self.size();
|
||||
egui::vec2(w as f32, h as f32)
|
||||
}
|
||||
|
||||
/// The debug name of the image, e.g. the file name.
|
||||
pub fn debug_name(&self) -> &str {
|
||||
&self.debug_name
|
||||
}
|
||||
|
||||
/// The texture id for this image.
|
||||
pub fn texture_id(&self, ctx: &egui::Context) -> egui::TextureId {
|
||||
self.texture
|
||||
.lock()
|
||||
.get_or_insert_with(|| {
|
||||
let image: &mut egui::ColorImage = &mut self.image.lock();
|
||||
let image = std::mem::take(image);
|
||||
ctx.load_texture(&self.debug_name, image, self.options)
|
||||
})
|
||||
.id()
|
||||
}
|
||||
|
||||
/// Show the image with the given maximum size.
|
||||
pub fn show_max_size(&self, ui: &mut egui::Ui, max_size: egui::Vec2) -> egui::Response {
|
||||
let mut desired_size = self.size_vec2();
|
||||
desired_size *= (max_size.x / desired_size.x).min(1.0);
|
||||
desired_size *= (max_size.y / desired_size.y).min(1.0);
|
||||
self.show_size(ui, desired_size)
|
||||
}
|
||||
|
||||
/// Show the image with the original size (one image pixel = one gui point).
|
||||
pub fn show(&self, ui: &mut egui::Ui) -> egui::Response {
|
||||
self.show_size(ui, self.size_vec2())
|
||||
}
|
||||
|
||||
/// Show the image with the given scale factor (1.0 = original size).
|
||||
pub fn show_scaled(&self, ui: &mut egui::Ui, scale: f32) -> egui::Response {
|
||||
self.show_size(ui, self.size_vec2() * scale)
|
||||
}
|
||||
|
||||
/// Show the image with the given size.
|
||||
pub fn show_size(&self, ui: &mut egui::Ui, desired_size: egui::Vec2) -> egui::Response {
|
||||
// We need to convert the SVG to a texture to display it:
|
||||
// Future improvement: tell backend to do mip-mapping of the image to
|
||||
// make it look smoother when downsized.
|
||||
ui.image((self.texture_id(ui.ctx()), desired_size))
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Load a (non-svg) image.
|
||||
@@ -214,6 +27,10 @@ pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, egui::lo
|
||||
let size = [image.width() as _, image.height() as _];
|
||||
let image_buffer = image.to_rgba8();
|
||||
let pixels = image_buffer.as_flat_samples();
|
||||
|
||||
// TODO(emilk): if this is a PNG, looks for DPI info to calculate the source size,
|
||||
// e.g. for screenshots taken on a high-DPI/retina display.
|
||||
|
||||
Ok(egui::ColorImage::from_rgba_unmultiplied(
|
||||
size,
|
||||
pixels.as_slice(),
|
||||
@@ -227,8 +44,11 @@ pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, egui::lo
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn load_svg_bytes(svg_bytes: &[u8]) -> Result<egui::ColorImage, String> {
|
||||
load_svg_bytes_with_size(svg_bytes, None)
|
||||
pub fn load_svg_bytes(
|
||||
svg_bytes: &[u8],
|
||||
options: &resvg::usvg::Options<'_>,
|
||||
) -> Result<egui::ColorImage, String> {
|
||||
load_svg_bytes_with_size(svg_bytes, Default::default(), options)
|
||||
}
|
||||
|
||||
/// Load an SVG and rasterize it into an egui image with a scaling parameter.
|
||||
@@ -240,51 +60,58 @@ pub fn load_svg_bytes(svg_bytes: &[u8]) -> Result<egui::ColorImage, String> {
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn load_svg_bytes_with_size(
|
||||
svg_bytes: &[u8],
|
||||
size_hint: Option<SizeHint>,
|
||||
size_hint: SizeHint,
|
||||
options: &resvg::usvg::Options<'_>,
|
||||
) -> Result<egui::ColorImage, String> {
|
||||
use resvg::tiny_skia::{IntSize, Pixmap};
|
||||
use resvg::usvg::{Options, Tree, TreeParsing};
|
||||
use egui::Vec2;
|
||||
use resvg::{
|
||||
tiny_skia::Pixmap,
|
||||
usvg::{Transform, Tree},
|
||||
};
|
||||
|
||||
profiling::function_scope!();
|
||||
|
||||
let opt = Options::default();
|
||||
let rtree = Tree::from_data(svg_bytes, options).map_err(|err| err.to_string())?;
|
||||
|
||||
let mut rtree = Tree::from_data(svg_bytes, &opt).map_err(|err| err.to_string())?;
|
||||
let source_size = Vec2::new(rtree.size().width(), rtree.size().height());
|
||||
|
||||
let mut size = rtree.size.to_int_size();
|
||||
match size_hint {
|
||||
None => (),
|
||||
Some(SizeHint::Size(w, h)) => {
|
||||
size = size.scale_to(
|
||||
IntSize::from_wh(w, h).ok_or_else(|| format!("Failed to scale SVG to {w}x{h}"))?,
|
||||
);
|
||||
}
|
||||
Some(SizeHint::Height(h)) => {
|
||||
size = size
|
||||
.scale_to_height(h)
|
||||
.ok_or_else(|| format!("Failed to scale SVG to height {h}"))?;
|
||||
}
|
||||
Some(SizeHint::Width(w)) => {
|
||||
size = size
|
||||
.scale_to_width(w)
|
||||
.ok_or_else(|| format!("Failed to scale SVG to width {w}"))?;
|
||||
}
|
||||
Some(SizeHint::Scale(z)) => {
|
||||
let z_inner = z.into_inner();
|
||||
size = size
|
||||
.scale_by(z_inner)
|
||||
.ok_or_else(|| format!("Failed to scale SVG by {z_inner}"))?;
|
||||
let scaled_size = match size_hint {
|
||||
SizeHint::Size {
|
||||
width,
|
||||
height,
|
||||
maintain_aspect_ratio,
|
||||
} => {
|
||||
if maintain_aspect_ratio {
|
||||
// As large as possible, without exceeding the given size:
|
||||
let mut size = source_size;
|
||||
size *= width as f32 / source_size.x;
|
||||
if size.y > height as f32 {
|
||||
size *= height as f32 / size.y;
|
||||
}
|
||||
size
|
||||
} else {
|
||||
Vec2::new(width as _, height as _)
|
||||
}
|
||||
}
|
||||
SizeHint::Height(h) => source_size * (h as f32 / source_size.y),
|
||||
SizeHint::Width(w) => source_size * (w as f32 / source_size.x),
|
||||
SizeHint::Scale(scale) => scale.into_inner() * source_size,
|
||||
};
|
||||
let (w, h) = (size.width(), size.height());
|
||||
|
||||
let scaled_size = scaled_size.round();
|
||||
let (w, h) = (scaled_size.x as u32, scaled_size.y as u32);
|
||||
|
||||
let mut pixmap =
|
||||
Pixmap::new(w, h).ok_or_else(|| format!("Failed to create SVG Pixmap of size {w}x{h}"))?;
|
||||
|
||||
rtree.size = size.to_size();
|
||||
resvg::Tree::from_usvg(&rtree).render(Default::default(), &mut pixmap.as_mut());
|
||||
resvg::render(
|
||||
&rtree,
|
||||
Transform::from_scale(w as f32 / source_size.x, h as f32 / source_size.y),
|
||||
&mut pixmap.as_mut(),
|
||||
);
|
||||
|
||||
let image = egui::ColorImage::from_rgba_unmultiplied([w as _, h as _], pixmap.data());
|
||||
let image = egui::ColorImage::from_rgba_premultiplied([w as _, h as _], pixmap.data())
|
||||
.with_source_size(source_size);
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui::{emath::GuiRounding, Id, Pos2, Rect, Response, Sense, Ui, UiBuilder, Vec2};
|
||||
use egui::{emath::GuiRounding as _, Id, Pos2, Rect, Response, Sense, Ui, UiBuilder, Vec2};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum CellSize {
|
||||
@@ -33,6 +33,7 @@ pub(crate) struct StripLayoutFlags {
|
||||
pub(crate) striped: bool,
|
||||
pub(crate) hovered: bool,
|
||||
pub(crate) selected: bool,
|
||||
pub(crate) overline: bool,
|
||||
|
||||
/// Used when we want to accruately measure the size of this cell.
|
||||
pub(crate) sizing_pass: bool,
|
||||
@@ -233,6 +234,14 @@ impl<'l> StripLayout<'l> {
|
||||
child_ui.style_mut().visuals.override_text_color = Some(stroke_color);
|
||||
}
|
||||
|
||||
if flags.overline {
|
||||
child_ui.painter().hline(
|
||||
max_rect.x_range(),
|
||||
max_rect.top(),
|
||||
child_ui.visuals().widgets.noninteractive.bg_stroke,
|
||||
);
|
||||
}
|
||||
|
||||
add_cell_contents(&mut child_ui);
|
||||
|
||||
child_ui
|
||||
|
||||
@@ -25,9 +25,6 @@ mod table;
|
||||
#[cfg(feature = "chrono")]
|
||||
pub use crate::datepicker::DatePickerButton;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[allow(deprecated)]
|
||||
pub use crate::image::RetainedImage;
|
||||
pub(crate) use crate::layout::StripLayout;
|
||||
pub use crate::sizing::Size;
|
||||
pub use crate::strip::*;
|
||||
|
||||
@@ -125,4 +125,8 @@ impl BytesLoader for EhttpLoader {
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn has_pending(&self) -> bool {
|
||||
self.cache.lock().values().any(|entry| entry.is_pending())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ impl BytesLoader for FileLoader {
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
let prev = cache.lock().insert(uri.clone(), Poll::Ready(result));
|
||||
assert!(matches!(prev, Some(Poll::Pending)));
|
||||
assert!(matches!(prev, Some(Poll::Pending)), "unexpected state");
|
||||
ctx.request_repaint();
|
||||
log::trace!("finished loading {uri:?}");
|
||||
}
|
||||
@@ -128,4 +128,8 @@ impl BytesLoader for FileLoader {
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn has_pending(&self) -> bool {
|
||||
self.cache.lock().values().any(|entry| entry.is_pending())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
use ahash::HashMap;
|
||||
use egui::{
|
||||
decode_animated_image_uri,
|
||||
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
|
||||
load::{Bytes, BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
|
||||
mutex::Mutex,
|
||||
ColorImage,
|
||||
};
|
||||
use image::ImageFormat;
|
||||
use std::{mem::size_of, path::Path, sync::Arc};
|
||||
use std::{mem::size_of, path::Path, sync::Arc, task::Poll};
|
||||
|
||||
type Entry = Result<Arc<ColorImage>, LoadError>;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::thread;
|
||||
|
||||
type Entry = Poll<Result<Arc<ColorImage>, String>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ImageCrateLoader {
|
||||
cache: Mutex<HashMap<String, Entry>>,
|
||||
cache: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
}
|
||||
|
||||
impl ImageCrateLoader {
|
||||
@@ -29,23 +32,27 @@ fn is_supported_uri(uri: &str) -> bool {
|
||||
};
|
||||
|
||||
// Uses only the enabled image crate features
|
||||
ImageFormat::all()
|
||||
.filter(ImageFormat::reading_enabled)
|
||||
.flat_map(ImageFormat::extensions_str)
|
||||
.any(|format_ext| ext == *format_ext)
|
||||
ImageFormat::from_extension(ext).is_some_and(|format| format.reading_enabled())
|
||||
}
|
||||
|
||||
fn is_supported_mime(mime: &str) -> bool {
|
||||
// This is the default mime type for binary files, so this might actually be a valid image,
|
||||
// let's relay on image's format guessing
|
||||
if mime == "application/octet-stream" {
|
||||
return true;
|
||||
// some mime types e.g. reflect binary files or mark the content as a download, which
|
||||
// may be a valid image or not, in this case, defer the decision on the format guessing
|
||||
// or the image crate and return true here
|
||||
let mimes_to_defer = [
|
||||
"application/octet-stream",
|
||||
"application/x-msdownload",
|
||||
"application/force-download",
|
||||
];
|
||||
for m in &mimes_to_defer {
|
||||
// use contains instead of direct equality, as e.g. encoding info might be appended
|
||||
if mime.contains(m) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Uses only the enabled image crate features
|
||||
ImageFormat::all()
|
||||
.filter(ImageFormat::reading_enabled)
|
||||
.map(|fmt| fmt.to_mime_type())
|
||||
.any(|format_mime| mime == format_mime)
|
||||
ImageFormat::from_mime_type(mime).is_some_and(|format| format.reading_enabled())
|
||||
}
|
||||
|
||||
impl ImageLoader for ImageCrateLoader {
|
||||
@@ -69,11 +76,72 @@ impl ImageLoader for ImageCrateLoader {
|
||||
return Err(LoadError::NotSupported);
|
||||
}
|
||||
|
||||
let mut cache = self.cache.lock();
|
||||
if let Some(entry) = cache.get(uri).cloned() {
|
||||
match entry {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[expect(clippy::unnecessary_wraps)] // needed here to match other return types
|
||||
fn load_image(
|
||||
ctx: &egui::Context,
|
||||
uri: &str,
|
||||
cache: &Arc<Mutex<HashMap<String, Entry>>>,
|
||||
bytes: &Bytes,
|
||||
) -> ImageLoadResult {
|
||||
let uri = uri.to_owned();
|
||||
cache.lock().insert(uri.clone(), Poll::Pending);
|
||||
|
||||
// Do the image parsing on a bg thread
|
||||
thread::Builder::new()
|
||||
.name(format!("egui_extras::ImageLoader::load({uri:?})"))
|
||||
.spawn({
|
||||
let ctx = ctx.clone();
|
||||
let cache = cache.clone();
|
||||
|
||||
let uri = uri.clone();
|
||||
let bytes = bytes.clone();
|
||||
move || {
|
||||
log::trace!("ImageLoader - started loading {uri:?}");
|
||||
let result = crate::image::load_image_bytes(&bytes)
|
||||
.map(Arc::new)
|
||||
.map_err(|err| err.to_string());
|
||||
log::trace!("ImageLoader - finished loading {uri:?}");
|
||||
let prev = cache.lock().insert(uri, Poll::Ready(result));
|
||||
debug_assert!(
|
||||
matches!(prev, Some(Poll::Pending)),
|
||||
"Expected previous state to be Pending"
|
||||
);
|
||||
|
||||
ctx.request_repaint();
|
||||
}
|
||||
})
|
||||
.expect("failed to spawn thread");
|
||||
|
||||
Ok(ImagePoll::Pending { size: None })
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn load_image(
|
||||
_ctx: &egui::Context,
|
||||
uri: &str,
|
||||
cache: &Arc<Mutex<HashMap<String, Entry>>>,
|
||||
bytes: &Bytes,
|
||||
) -> ImageLoadResult {
|
||||
let mut cache_lock = cache.lock();
|
||||
log::trace!("started loading {uri:?}");
|
||||
let result = crate::image::load_image_bytes(bytes)
|
||||
.map(Arc::new)
|
||||
.map_err(|err| err.to_string());
|
||||
log::trace!("finished loading {uri:?}");
|
||||
cache_lock.insert(uri.into(), std::task::Poll::Ready(result.clone()));
|
||||
match result {
|
||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
||||
Err(err) => Err(err),
|
||||
Err(err) => Err(LoadError::Loading(err)),
|
||||
}
|
||||
}
|
||||
|
||||
let entry = self.cache.lock().get(uri).cloned();
|
||||
if let Some(entry) = entry {
|
||||
match entry {
|
||||
Poll::Ready(Ok(image)) => Ok(ImagePoll::Ready { image }),
|
||||
Poll::Ready(Err(err)) => Err(LoadError::Loading(err)),
|
||||
Poll::Pending => Ok(ImagePoll::Pending { size: None }),
|
||||
}
|
||||
} else {
|
||||
match ctx.try_load_bytes(uri) {
|
||||
@@ -86,19 +154,7 @@ impl ImageLoader for ImageCrateLoader {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if bytes.starts_with(b"version https://git-lfs") {
|
||||
return Err(LoadError::FormatNotSupported {
|
||||
detected_format: Some("git-lfs".to_owned()),
|
||||
});
|
||||
}
|
||||
|
||||
// (3)
|
||||
log::trace!("started loading {uri:?}");
|
||||
let result = crate::image::load_image_bytes(&bytes).map(Arc::new);
|
||||
log::trace!("finished loading {uri:?}");
|
||||
cache.insert(uri.into(), result.clone());
|
||||
result.map(|image| ImagePoll::Ready { image })
|
||||
load_image(ctx, uri, &self.cache, &bytes)
|
||||
}
|
||||
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
|
||||
Err(err) => Err(err),
|
||||
@@ -119,11 +175,16 @@ impl ImageLoader for ImageCrateLoader {
|
||||
.lock()
|
||||
.values()
|
||||
.map(|result| match result {
|
||||
Ok(image) => image.pixels.len() * size_of::<egui::Color32>(),
|
||||
Err(err) => err.byte_size(),
|
||||
Poll::Ready(Ok(image)) => image.pixels.len() * size_of::<egui::Color32>(),
|
||||
Poll::Ready(Err(err)) => err.len(),
|
||||
Poll::Pending => 0,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn has_pending(&self) -> bool {
|
||||
self.cache.lock().values().any(|result| result.is_pending())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
use std::{borrow::Cow, mem::size_of, path::Path, sync::Arc};
|
||||
use std::{
|
||||
mem::size_of,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering::Relaxed},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use ahash::HashMap;
|
||||
|
||||
@@ -8,11 +14,15 @@ use egui::{
|
||||
ColorImage,
|
||||
};
|
||||
|
||||
type Entry = Result<Arc<ColorImage>, String>;
|
||||
struct Entry {
|
||||
last_used: AtomicU64,
|
||||
result: Result<Arc<ColorImage>, String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SvgLoader {
|
||||
cache: Mutex<HashMap<(Cow<'static, str>, SizeHint), Entry>>,
|
||||
pass_index: AtomicU64,
|
||||
cache: Mutex<HashMap<String, HashMap<SizeHint, Entry>>>,
|
||||
options: resvg::usvg::Options<'static>,
|
||||
}
|
||||
|
||||
impl SvgLoader {
|
||||
@@ -20,11 +30,24 @@ impl SvgLoader {
|
||||
}
|
||||
|
||||
fn is_supported(uri: &str) -> bool {
|
||||
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else {
|
||||
return false;
|
||||
};
|
||||
uri.ends_with(".svg")
|
||||
}
|
||||
|
||||
ext == "svg"
|
||||
impl Default for SvgLoader {
|
||||
fn default() -> Self {
|
||||
// opt is mutated when `svg_text` feature flag is enabled
|
||||
#[allow(unused_mut, clippy::allow_attributes)]
|
||||
let mut options = resvg::usvg::Options::default();
|
||||
|
||||
#[cfg(feature = "svg_text")]
|
||||
options.fontdb_mut().load_system_fonts();
|
||||
|
||||
Self {
|
||||
pass_index: AtomicU64::new(0),
|
||||
cache: Mutex::new(HashMap::default()),
|
||||
options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageLoader for SvgLoader {
|
||||
@@ -38,20 +61,32 @@ impl ImageLoader for SvgLoader {
|
||||
}
|
||||
|
||||
let mut cache = self.cache.lock();
|
||||
// We can't avoid the `uri` clone here without unsafe code.
|
||||
if let Some(entry) = cache.get(&(Cow::Borrowed(uri), size_hint)).cloned() {
|
||||
match entry {
|
||||
let bucket = cache.entry(uri.to_owned()).or_default();
|
||||
|
||||
if let Some(entry) = bucket.get(&size_hint) {
|
||||
entry
|
||||
.last_used
|
||||
.store(self.pass_index.load(Relaxed), Relaxed);
|
||||
match entry.result.clone() {
|
||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
||||
Err(err) => Err(LoadError::Loading(err)),
|
||||
}
|
||||
} else {
|
||||
match ctx.try_load_bytes(uri) {
|
||||
Ok(BytesPoll::Ready { bytes, .. }) => {
|
||||
log::trace!("started loading {uri:?}");
|
||||
let result = crate::image::load_svg_bytes_with_size(&bytes, Some(size_hint))
|
||||
.map(Arc::new);
|
||||
log::trace!("finished loading {uri:?}");
|
||||
cache.insert((Cow::Owned(uri.to_owned()), size_hint), result.clone());
|
||||
log::trace!("Started loading {uri:?}");
|
||||
let result =
|
||||
crate::image::load_svg_bytes_with_size(&bytes, size_hint, &self.options)
|
||||
.map(Arc::new);
|
||||
|
||||
log::trace!("Finished loading {uri:?}");
|
||||
bucket.insert(
|
||||
size_hint,
|
||||
Entry {
|
||||
last_used: AtomicU64::new(self.pass_index.load(Relaxed)),
|
||||
result: result.clone(),
|
||||
},
|
||||
);
|
||||
match result {
|
||||
Ok(image) => Ok(ImagePoll::Ready { image }),
|
||||
Err(err) => Err(LoadError::Loading(err)),
|
||||
@@ -64,7 +99,7 @@ impl ImageLoader for SvgLoader {
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
self.cache.lock().retain(|(u, _), _| u != uri);
|
||||
self.cache.lock().retain(|key, _| key != uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
@@ -75,12 +110,28 @@ impl ImageLoader for SvgLoader {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|result| match result {
|
||||
.flat_map(|bucket| bucket.values())
|
||||
.map(|entry| match &entry.result {
|
||||
Ok(image) => image.pixels.len() * size_of::<egui::Color32>(),
|
||||
Err(err) => err.len(),
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn end_pass(&self, pass_index: u64) {
|
||||
self.pass_index.store(pass_index, Relaxed);
|
||||
let mut cache = self.cache.lock();
|
||||
cache.retain(|_key, bucket| {
|
||||
if 2 <= bucket.len() {
|
||||
// There are multiple images of the same URI (e.g. SVGs of different scales).
|
||||
// This could be because someone has an SVG in a resizable container,
|
||||
// and so we get a lot of different sizes of it.
|
||||
// This could wast RAM, so we remove the ones that are not used in this frame.
|
||||
bucket.retain(|_, texture| pass_index <= texture.last_used.load(Relaxed) + 1);
|
||||
}
|
||||
!bucket.is_empty()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,7 +5,7 @@ use egui::{
|
||||
mutex::Mutex,
|
||||
ColorImage, FrameDurations, Id,
|
||||
};
|
||||
use image::{codecs::webp::WebPDecoder, AnimationDecoder as _, ColorType, ImageDecoder, Rgba};
|
||||
use image::{codecs::webp::WebPDecoder, AnimationDecoder as _, ColorType, ImageDecoder as _, Rgba};
|
||||
use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -32,7 +32,10 @@ impl Size {
|
||||
|
||||
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`.
|
||||
pub fn relative(fraction: f32) -> Self {
|
||||
debug_assert!(0.0 <= fraction && fraction <= 1.0);
|
||||
debug_assert!(
|
||||
0.0 <= fraction && fraction <= 1.0,
|
||||
"fraction should be in the range [0, 1], but was {fraction}"
|
||||
);
|
||||
Self::Relative {
|
||||
fraction,
|
||||
range: Rangef::new(0.0, f32::INFINITY),
|
||||
@@ -121,7 +124,10 @@ impl Sizing {
|
||||
.map(|&size| match size {
|
||||
Size::Absolute { initial, .. } => initial,
|
||||
Size::Relative { fraction, range } => {
|
||||
assert!(0.0 <= fraction && fraction <= 1.0);
|
||||
assert!(
|
||||
0.0 <= fraction && fraction <= 1.0,
|
||||
"fraction should be in the range [0, 1], but was {fraction}"
|
||||
);
|
||||
range.clamp(length * fraction)
|
||||
}
|
||||
Size::Remainder { .. } => {
|
||||
|
||||
@@ -33,6 +33,7 @@ pub fn highlight(
|
||||
// performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available
|
||||
// (ui.ctx(), ui.style()) can be used
|
||||
|
||||
#[expect(non_local_definitions)]
|
||||
impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
||||
fn compute(
|
||||
&mut self,
|
||||
@@ -284,7 +285,7 @@ impl CodeTheme {
|
||||
impl CodeTheme {
|
||||
// The syntect version takes it by value. This could be avoided by specializing the from_style
|
||||
// function, but at the cost of more code duplication.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[expect(clippy::needless_pass_by_value)]
|
||||
fn dark_with_font_id(font_id: egui::FontId) -> Self {
|
||||
use egui::{Color32, TextFormat};
|
||||
Self {
|
||||
@@ -301,7 +302,7 @@ impl CodeTheme {
|
||||
}
|
||||
|
||||
// The syntect version takes it by value
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[expect(clippy::needless_pass_by_value)]
|
||||
fn light_with_font_id(font_id: egui::FontId) -> Self {
|
||||
use egui::{Color32, TextFormat};
|
||||
Self {
|
||||
@@ -412,7 +413,6 @@ impl Default for Highlighter {
|
||||
}
|
||||
|
||||
impl Highlighter {
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
fn highlight(
|
||||
&self,
|
||||
font_id: egui::FontId,
|
||||
@@ -490,8 +490,15 @@ impl Highlighter {
|
||||
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
|
||||
let whole_start = whole.as_ptr() as usize;
|
||||
let range_start = range.as_ptr() as usize;
|
||||
assert!(whole_start <= range_start);
|
||||
assert!(range_start + range.len() <= whole_start + whole.len());
|
||||
assert!(
|
||||
whole_start <= range_start,
|
||||
"range must be within whole, but was {range}"
|
||||
);
|
||||
assert!(
|
||||
range_start + range.len() <= whole_start + whole.len(),
|
||||
"range_start + range length must be smaller than whole_start + whole length, but was {}",
|
||||
range_start + range.len()
|
||||
);
|
||||
let offset = range_start - whole_start;
|
||||
offset..(offset + range.len())
|
||||
}
|
||||
@@ -504,7 +511,7 @@ struct Highlighter {}
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
impl Highlighter {
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
#[expect(clippy::unused_self)]
|
||||
fn highlight_impl(
|
||||
&self,
|
||||
theme: &CodeTheme,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! Takes all available height, so if you want something below the table, put it in a strip.
|
||||
|
||||
use egui::{
|
||||
scroll_area::{ScrollAreaOutput, ScrollBarVisibility},
|
||||
scroll_area::{ScrollAreaOutput, ScrollBarVisibility, ScrollSource},
|
||||
Align, Id, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2, Vec2b,
|
||||
};
|
||||
|
||||
@@ -507,6 +507,7 @@ impl<'a> TableBuilder<'a> {
|
||||
striped: false,
|
||||
hovered: false,
|
||||
selected: false,
|
||||
overline: false,
|
||||
response: &mut response,
|
||||
});
|
||||
layout.allocate_rect();
|
||||
@@ -744,7 +745,10 @@ impl Table<'_> {
|
||||
|
||||
let mut scroll_area = ScrollArea::new([false, vscroll])
|
||||
.id_salt(state_id.with("__scroll_area"))
|
||||
.drag_to_scroll(drag_to_scroll)
|
||||
.scroll_source(ScrollSource {
|
||||
drag: drag_to_scroll,
|
||||
..Default::default()
|
||||
})
|
||||
.stick_to_bottom(stick_to_bottom)
|
||||
.min_scrolled_height(min_scrolled_height)
|
||||
.max_height(max_scroll_height)
|
||||
@@ -990,6 +994,7 @@ impl<'a> TableBody<'a> {
|
||||
striped: self.striped && self.row_index % 2 == 0,
|
||||
hovered: self.hovered_row_index == Some(self.row_index),
|
||||
selected: false,
|
||||
overline: false,
|
||||
response: &mut response,
|
||||
});
|
||||
self.capture_hover_state(&response, self.row_index);
|
||||
@@ -1071,6 +1076,7 @@ impl<'a> TableBody<'a> {
|
||||
striped: self.striped && (row_index + self.row_index) % 2 == 0,
|
||||
hovered: self.hovered_row_index == Some(row_index),
|
||||
selected: false,
|
||||
overline: false,
|
||||
response: &mut response,
|
||||
});
|
||||
self.capture_hover_state(&response, row_index);
|
||||
@@ -1152,6 +1158,7 @@ impl<'a> TableBody<'a> {
|
||||
striped: self.striped && (row_index + self.row_index) % 2 == 0,
|
||||
hovered: self.hovered_row_index == Some(row_index),
|
||||
selected: false,
|
||||
overline: false,
|
||||
response: &mut response,
|
||||
});
|
||||
self.capture_hover_state(&response, row_index);
|
||||
@@ -1173,6 +1180,7 @@ impl<'a> TableBody<'a> {
|
||||
height: row_height,
|
||||
striped: self.striped && (row_index + self.row_index) % 2 == 0,
|
||||
hovered: self.hovered_row_index == Some(row_index),
|
||||
overline: false,
|
||||
selected: false,
|
||||
response: &mut response,
|
||||
});
|
||||
@@ -1260,6 +1268,7 @@ pub struct TableRow<'a, 'b> {
|
||||
striped: bool,
|
||||
hovered: bool,
|
||||
selected: bool,
|
||||
overline: bool,
|
||||
|
||||
response: &'b mut Option<Response>,
|
||||
}
|
||||
@@ -1297,6 +1306,7 @@ impl TableRow<'_, '_> {
|
||||
striped: self.striped,
|
||||
hovered: self.hovered,
|
||||
selected: self.selected,
|
||||
overline: self.overline,
|
||||
sizing_pass: auto_size_this_frame || self.layout.ui.is_sizing_pass(),
|
||||
};
|
||||
|
||||
@@ -1333,6 +1343,13 @@ impl TableRow<'_, '_> {
|
||||
self.hovered = hovered;
|
||||
}
|
||||
|
||||
/// Set the overline state for this row. The overline is a line above the row,
|
||||
/// usable for e.g. visually grouping rows.
|
||||
#[inline]
|
||||
pub fn set_overline(&mut self, overline: bool) {
|
||||
self.overline = overline;
|
||||
}
|
||||
|
||||
/// Returns a union of the [`Response`]s of the cells added to the row up to this point.
|
||||
///
|
||||
/// You need to add at least one row to the table before calling this function.
|
||||
|
||||
Reference in New Issue
Block a user