1
0
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:
lucasmerlin
2025-06-16 09:52:22 +02:00
389 changed files with 8660 additions and 3857 deletions

View File

@@ -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 {

View File

@@ -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};

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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::*;

View File

@@ -125,4 +125,8 @@ impl BytesLoader for EhttpLoader {
})
.sum()
}
fn has_pending(&self) -> bool {
self.cache.lock().values().any(|entry| entry.is_pending())
}
}

View File

@@ -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())
}
}

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -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 { .. } => {

View File

@@ -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,

View File

@@ -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.