1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-03 23:30:04 -04:00

Merge branch 'main' into lucas/experiments/helpful-id-debug

# Conflicts:
#	crates/egui/src/containers/combo_box.rs
#	crates/egui/src/id.rs
#	crates/egui/src/lib.rs
#	crates/egui/src/ui.rs
#	crates/egui/src/ui_builder.rs
This commit is contained in:
lucasmerlin
2026-02-17 17:13:12 +01:00
594 changed files with 22240 additions and 11164 deletions

View File

@@ -5,6 +5,57 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.3 - 2025-12-11
* Bump `ehttp` to 0.6.0 [#7757](https://github.com/emilk/egui/pull/7757) by [@jprochazk](https://github.com/jprochazk)
## 0.33.2 - 2025-11-13
Nothing new
## 0.33.0 - 2025-10-09
* Fix: use unique id for resize columns in `Table` [#7414](https://github.com/emilk/egui/pull/7414) by [@zezic](https://github.com/zezic)
* Feat: Add serde serialization to SyntectSettings [#7506](https://github.com/emilk/egui/pull/7506) by [@bircni](https://github.com/bircni)
* Make individual egui_extras image loaders public [#7551](https://github.com/emilk/egui/pull/7551) by [@lucasmerlin](https://github.com/lucasmerlin)
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
## 0.32.3 - 2025-09-12
* Fix deadlock in `FileLoader` and `EhttpLoader` [#7515](https://github.com/emilk/egui/pull/7515) by [@emilk](https://github.com/emilk)
## 0.32.2 - 2025-09-04
* Fix memory leak when `forget_image` is called while loading [#7380](https://github.com/emilk/egui/pull/7380) by [@Vanadiae](https://github.com/Vanadiae)
* Fix deadlock in `ImageLoader`, `FileLoader`, `EhttpLoader` [#7494](https://github.com/emilk/egui/pull/7494) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.32.1 - 2025-08-15
Nothing new
## 0.32.0 - 2025-07-10 - Improved SVG support
### ⭐ Added
* Allow loading multi-MIME formats using the image_loader [#5769](https://github.com/emilk/egui/pull/5769) by [@MYDIH](https://github.com/MYDIH)
* Make ImageLoader use background thread [#5394](https://github.com/emilk/egui/pull/5394) by [@bircni](https://github.com/bircni)
* Add overline option for Table rows [#5637](https://github.com/emilk/egui/pull/5637) by [@akx](https://github.com/akx)
* Support text in SVGs [#5979](https://github.com/emilk/egui/pull/5979) by [@cernec1999](https://github.com/cernec1999)
* Enable setting DatePickerButton start and end year explicitly [#7061](https://github.com/emilk/egui/pull/7061) by [@zachbateman](https://github.com/zachbateman)
* Support custom syntect settings in syntax highlighter [#7084](https://github.com/emilk/egui/pull/7084) by [@mkeeter](https://github.com/mkeeter)
### 🔧 Changed
* Use enum-map serde feature only when serde is enabled [#5748](https://github.com/emilk/egui/pull/5748) by [@tyssyt](https://github.com/tyssyt)
* Better define the meaning of `SizeHint` [#7079](https://github.com/emilk/egui/pull/7079) by [@emilk](https://github.com/emilk)
### 🔥 Removed
* Remove things that have been deprecated for over a year [#7099](https://github.com/emilk/egui/pull/7099) by [@emilk](https://github.com/emilk)
### 🐛 Fixed
* Refactor MIME type support detection in image loader to allow for deferred handling and appended encoding info [#5686](https://github.com/emilk/egui/pull/5686) by [@markusdd](https://github.com/markusdd)
* Fix incorrect color fringe colors on SVG:s [#7069](https://github.com/emilk/egui/pull/7069) by [@emilk](https://github.com/emilk)
* Fix sometimes blurry SVGs [#7071](https://github.com/emilk/egui/pull/7071) by [@emilk](https://github.com/emilk)
* Fix crash in `egui_extras::FileLoader` after `forget_image` [#6995](https://github.com/emilk/egui/pull/6995) by [@bircni](https://github.com/bircni)
## 0.31.1 - 2025-03-05
* Fix image_loader for animated image types [#5688](https://github.com/emilk/egui/pull/5688) by [@BSteffaniak](https://github.com/BSteffaniak)

View File

@@ -30,7 +30,7 @@ rustdoc-args = ["--generate-link-to-definition"]
[features]
default = ["dep:mime_guess2"]
## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`).
## Shorthand for enabling all the different types of image loaders.
all_loaders = ["file", "http", "image", "svg", "gif", "webp"]
## Enable [`DatePickerButton`] widget.
@@ -62,6 +62,9 @@ serde = ["egui/serde", "enum-map/serde", "dep:serde"]
## Support loading svg images.
svg = ["resvg"]
## Support rendering text in svg images.
svg_text = ["svg", "resvg/text", "resvg/system-fonts"]
## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect).
syntect = ["dep:syntect"]
@@ -70,7 +73,7 @@ syntect = ["dep:syntect"]
egui = { workspace = true, default-features = false }
ahash.workspace = true
enum-map = "2"
enum-map.workspace = true
log.workspace = true
profiling.workspace = true
@@ -80,12 +83,7 @@ profiling.workspace = true
serde = { workspace = true, optional = true }
# Date operations needed for datepicker widget
chrono = { version = "0.4", optional = true, default-features = false, features = [
"clock",
"js-sys",
"std",
"wasmbind",
] }
chrono = { workspace = true, optional = true, features = ["clock", "js-sys", "std", "wasmbind"] }
## Enable this when generating docs.
document-features = { workspace = true, optional = true }
@@ -93,15 +91,13 @@ document-features = { workspace = true, optional = true }
image = { workspace = true, optional = true }
# file feature
mime_guess2 = { version = "2", optional = true, default-features = false }
mime_guess2 = { workspace = true, optional = true }
syntect = { version = "5", optional = true, default-features = false, features = [
"default-fancy",
] }
# syntax highlighting
syntect = { workspace = true, optional = true, features = ["default-fancy"] }
# svg feature
resvg = { version = "0.37", optional = true, default-features = false }
resvg = { workspace = true, optional = true }
# http feature
ehttp = { version = "0.5", optional = true, default-features = false }
ehttp = { workspace = true, optional = true }

View File

@@ -1,6 +1,7 @@
use super::popup::DatePickerPopup;
use chrono::NaiveDate;
use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Ui, Widget};
use std::ops::RangeInclusive;
#[derive(Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -19,6 +20,7 @@ pub struct DatePickerButton<'a> {
show_icon: bool,
format: String,
highlight_weekends: bool,
start_end_years: Option<RangeInclusive<i32>>,
}
impl<'a> DatePickerButton<'a> {
@@ -33,6 +35,7 @@ impl<'a> DatePickerButton<'a> {
show_icon: true,
format: "%Y-%m-%d".to_owned(),
highlight_weekends: true,
start_end_years: None,
}
}
@@ -101,6 +104,17 @@ impl<'a> DatePickerButton<'a> {
self.highlight_weekends = highlight_weekends;
self
}
/// Set the start and end years for the date picker. (Default: today's year - 100 to today's year + 10)
/// This will limit the years you can choose from in the dropdown to the specified range.
///
/// For example, if you want to provide the range of years from 2000 to 2035, you can use:
/// `start_end_years(2000..=2035)`.
#[inline]
pub fn start_end_years(mut self, start_end_years: RangeInclusive<i32>) -> Self {
self.start_end_years = Some(start_end_years);
self
}
}
impl Widget for DatePickerButton<'_> {
@@ -167,6 +181,7 @@ impl Widget for DatePickerButton<'_> {
calendar: self.calendar,
calendar_week: self.calendar_week,
highlight_weekends: self.highlight_weekends,
start_end_years: self.start_end_years,
}
.draw(ui)
})
@@ -177,7 +192,11 @@ impl Widget for DatePickerButton<'_> {
button_response.mark_changed();
}
// We don't want to close our popup if any other popup is open, since other popups would
// most likely be the combo boxes in the date picker.
let any_popup_open = ui.any_popup_open();
if !button_response.clicked()
&& !any_popup_open
&& (ui.input(|i| i.key_pressed(Key::Escape)) || area_response.clicked_elsewhere())
{
button_state.picker_visible = false;

View File

@@ -1,8 +1,10 @@
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
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};
@@ -35,6 +35,7 @@ pub(crate) struct DatePickerPopup<'a> {
pub calendar: bool,
pub calendar_week: bool,
pub highlight_weekends: bool,
pub start_end_years: Option<std::ops::RangeInclusive<i32>>,
}
impl DatePickerPopup<'_> {
@@ -84,7 +85,11 @@ impl DatePickerPopup<'_> {
ComboBox::from_id_salt("date_picker_year")
.selected_text(popup_state.year.to_string())
.show_ui(ui, |ui| {
for year in today.year() - 100..today.year() + 10 {
let (start_year, end_year) = match &self.start_end_years {
Some(range) => (*range.start(), *range.end()),
None => (today.year() - 100, today.year() + 10),
};
for year in start_year..=end_year {
if ui
.selectable_value(
&mut popup_state.year,
@@ -331,7 +336,7 @@ impl DatePickerPopup<'_> {
if day.month() != popup_state.month {
text_color =
text_color.linear_multiply(0.5);
};
}
let button_response = ui.add(
Button::new(

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};
use egui::{Id, Pos2, Rect, Response, Sense, Ui, UiBuilder, emath::GuiRounding as _};
#[derive(Clone, Copy)]
pub(crate) enum CellSize {
@@ -33,8 +33,9 @@ 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.
/// Used when we want to accurately measure the size of this cell.
pub(crate) sizing_pass: bool,
}
@@ -161,7 +162,7 @@ impl<'l> StripLayout<'l> {
} else if flags.clip {
max_rect
} else {
max_rect.union(used_rect)
max_rect | used_rect
};
self.set_pos(allocation_rect);
@@ -232,6 +233,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

@@ -6,8 +6,7 @@
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
#![expect(clippy::manual_range_contains)]
#[cfg(feature = "chrono")]
mod datepicker;
@@ -17,7 +16,7 @@ pub mod syntax_highlighting;
#[doc(hidden)]
pub mod image;
mod layout;
mod loaders;
pub mod loaders;
mod sizing;
mod strip;
mod table;
@@ -25,9 +24,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

@@ -63,9 +63,9 @@ pub fn install_image_loaders(ctx: &egui::Context) {
}
#[cfg(feature = "http")]
if !ctx.is_loader_installed(self::ehttp_loader::EhttpLoader::ID) {
if !ctx.is_loader_installed(self::http_loader::EhttpLoader::ID) {
ctx.add_bytes_loader(std::sync::Arc::new(
self::ehttp_loader::EhttpLoader::default(),
self::http_loader::EhttpLoader::default(),
));
log::trace!("installed EhttpLoader");
}
@@ -108,16 +108,16 @@ pub fn install_image_loaders(ctx: &egui::Context) {
}
#[cfg(not(target_arch = "wasm32"))]
mod file_loader;
pub mod file_loader;
#[cfg(feature = "http")]
mod ehttp_loader;
pub mod http_loader;
#[cfg(feature = "gif")]
mod gif_loader;
pub mod gif_loader;
#[cfg(feature = "image")]
mod image_loader;
pub mod image_loader;
#[cfg(feature = "svg")]
mod svg_loader;
pub mod svg_loader;
#[cfg(feature = "webp")]
mod webp_loader;
pub mod webp_loader;

View File

@@ -75,7 +75,7 @@ impl BytesLoader for FileLoader {
.name(format!("egui_extras::FileLoader::load({uri:?})"))
.spawn({
let ctx = ctx.clone();
let cache = self.cache.clone();
let cache = Arc::clone(&self.cache);
let uri = uri.to_owned();
move || {
let result = match std::fs::read(&path) {
@@ -95,10 +95,23 @@ 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)), "unexpected state");
ctx.request_repaint();
log::trace!("finished loading {uri:?}");
let repaint = {
let mut cache = cache.lock();
if let std::collections::hash_map::Entry::Occupied(mut entry) = cache.entry(uri.clone()) {
let entry = entry.get_mut();
*entry = Poll::Ready(result);
log::trace!("Finished loading {uri:?}");
true
} else {
log::trace!("Canceled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading.");
false
}
};
// We may not lock Context while the cache lock is held (see ImageLoader::load
// for details).
if repaint {
ctx.request_repaint();
}
}
})
.expect("failed to spawn thread");
@@ -128,4 +141,8 @@ impl BytesLoader for FileLoader {
})
.sum()
}
fn has_pending(&self) -> bool {
self.cache.lock().values().any(|entry| entry.is_pending())
}
}

View File

@@ -1,9 +1,8 @@
use ahash::HashMap;
use egui::{
decode_animated_image_uri, has_gif_magic_header,
ColorImage, FrameDurations, Id, decode_animated_image_uri, has_gif_magic_header,
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
mutex::Mutex,
ColorImage, FrameDurations, Id,
};
use image::AnimationDecoder as _;
use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};
@@ -54,7 +53,7 @@ impl AnimatedImage {
/// Gets image at index
pub fn get_image(&self, index: usize) -> Arc<ColorImage> {
self.frames[index % self.frames.len()].clone()
Arc::clone(&self.frames[index % self.frames.len()])
}
}
type Entry = Result<Arc<AnimatedImage>, String>;

View File

@@ -19,13 +19,13 @@ impl File {
return Err(format!(
"failed to load {uri:?}: {} {} {response_text}",
response.status, response.status_text
))
));
}
None => {
return Err(format!(
"failed to load {uri:?}: {} {}",
response.status, response.status_text
))
));
}
}
}
@@ -84,7 +84,7 @@ impl BytesLoader for EhttpLoader {
ehttp::fetch(ehttp::Request::get(uri.clone()), {
let ctx = ctx.clone();
let cache = self.cache.clone();
let cache = Arc::clone(&self.cache);
move |response| {
let result = match response {
Ok(response) => File::from_response(&uri, response),
@@ -94,9 +94,27 @@ impl BytesLoader for EhttpLoader {
Err(format!("Failed to load {uri:?}"))
}
};
log::trace!("finished loading {uri:?}");
cache.lock().insert(uri, Poll::Ready(result));
ctx.request_repaint();
let repaint = {
let mut cache = cache.lock();
if let std::collections::hash_map::Entry::Occupied(mut entry) =
cache.entry(uri.clone())
{
let entry = entry.get_mut();
*entry = Poll::Ready(result);
log::trace!("Finished loading {uri:?}");
true
} else {
log::trace!(
"Canceled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading."
);
false
}
};
// We may not lock Context while the cache lock is held (see ImageLoader::load
// for details).
if repaint {
ctx.request_repaint();
}
}
});
@@ -125,4 +143,8 @@ impl BytesLoader for EhttpLoader {
})
.sum()
}
fn has_pending(&self) -> bool {
self.cache.lock().values().any(|entry| entry.is_pending())
}
}

View File

@@ -1,9 +1,8 @@
use ahash::HashMap;
use egui::{
decode_animated_image_uri,
ColorImage, decode_animated_image_uri,
load::{Bytes, BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
mutex::Mutex,
ColorImage,
};
use image::ImageFormat;
use std::{mem::size_of, path::Path, sync::Arc, task::Poll};
@@ -51,8 +50,11 @@ fn is_supported_mime(mime: &str) -> bool {
}
}
// Some servers may return a media type with an optional parameter, e.g. "image/jpeg; charset=utf-8".
let (mime_type, _) = mime.split_once(';').unwrap_or((mime, ""));
// Uses only the enabled image crate features
ImageFormat::from_mime_type(mime).is_some_and(|format| format.reading_enabled())
ImageFormat::from_mime_type(mime_type).is_some_and(|format| format.reading_enabled())
}
impl ImageLoader for ImageCrateLoader {
@@ -77,7 +79,7 @@ impl ImageLoader for ImageCrateLoader {
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::unnecessary_wraps)] // needed here to match other return types
#[expect(clippy::unnecessary_wraps)] // needed here to match other return types
fn load_image(
ctx: &egui::Context,
uri: &str,
@@ -92,7 +94,7 @@ impl ImageLoader for ImageCrateLoader {
.name(format!("egui_extras::ImageLoader::load({uri:?})"))
.spawn({
let ctx = ctx.clone();
let cache = cache.clone();
let cache = Arc::clone(cache);
let uri = uri.clone();
let bytes = bytes.clone();
@@ -101,14 +103,29 @@ impl ImageLoader for ImageCrateLoader {
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"
);
let repaint = {
let mut cache = cache.lock();
ctx.request_repaint();
if let std::collections::hash_map::Entry::Occupied(mut entry) = cache.entry(uri.clone()) {
let entry = entry.get_mut();
*entry = Poll::Ready(result);
log::trace!("ImageLoader - finished loading {uri:?}");
true
} else {
log::trace!("ImageLoader - canceled loading {uri:?}\nNote: This can happen if `forget_image` is called while the image is still loading.");
false
}
};
// We may not lock Context while the cache lock is held, since this can
// deadlock.
// Example deadlock scenario:
// - loader thread: lock cache
// - main thread: lock ctx (e.g. in `Context::has_pending_images`)
// - loader thread: try to lock ctx (in `request_repaint`)
// - main thread: try to lock cache (from `Self::has_pending`)
if repaint {
ctx.request_repaint();
}
}
})
.expect("failed to spawn thread");
@@ -147,12 +164,12 @@ impl ImageLoader for ImageCrateLoader {
match ctx.try_load_bytes(uri) {
Ok(BytesPoll::Ready { bytes, mime, .. }) => {
// (2)
if let Some(mime) = mime {
if !is_supported_mime(&mime) {
return Err(LoadError::FormatNotSupported {
detected_format: Some(mime),
});
}
if let Some(mime) = mime
&& !is_supported_mime(&mime)
{
return Err(LoadError::FormatNotSupported {
detected_format: Some(mime),
});
}
load_image(ctx, uri, &self.cache, &bytes)
}
@@ -181,6 +198,10 @@ impl ImageLoader for ImageCrateLoader {
})
.sum()
}
fn has_pending(&self) -> bool {
self.cache.lock().values().any(|result| result.is_pending())
}
}
#[cfg(test)]

View File

@@ -1,18 +1,28 @@
use std::{borrow::Cow, mem::size_of, path::Path, sync::Arc};
use std::{
mem::size_of,
sync::{
Arc,
atomic::{AtomicU64, Ordering::Relaxed},
},
};
use ahash::HashMap;
use egui::{
ColorImage,
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
mutex::Mutex,
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(clippy::allow_attributes, unused_mut)]
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

@@ -1,11 +1,10 @@
use ahash::HashMap;
use egui::{
decode_animated_image_uri, has_webp_header,
ColorImage, FrameDurations, Id, decode_animated_image_uri, has_webp_header,
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
mutex::Mutex,
ColorImage, FrameDurations, Id,
};
use image::{codecs::webp::WebPDecoder, AnimationDecoder as _, ColorType, ImageDecoder, Rgba};
use image::{AnimationDecoder as _, ColorType, ImageDecoder as _, Rgba, codecs::webp::WebPDecoder};
use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration};
#[derive(Clone)]
@@ -55,7 +54,7 @@ impl WebP {
unreachable => {
return Err(format!(
"Unreachable WebP color type, expected Rgb8/Rgba8, got {unreachable:?}"
))
));
}
};
@@ -76,7 +75,7 @@ impl WebP {
fn get_image(&self, frame_index: usize) -> Arc<ColorImage> {
match self {
Self::Static(image) => image.clone(),
Self::Static(image) => Arc::clone(image),
Self::Animated(animation) => animation.get_image_by_index(frame_index),
}
}
@@ -109,7 +108,7 @@ impl AnimatedImage {
}
pub fn get_image_by_index(&self, index: usize) -> Arc<ColorImage> {
self.frames[index % self.frames.len()].clone()
Arc::clone(&self.frames[index % self.frames.len()])
}
}

View File

@@ -144,11 +144,11 @@ impl Sizing {
let mut remainder_length = length - sum_non_remainder;
let avg_remainder_length = 0.0f32.max(remainder_length / num_remainders as f32).floor();
for &size in &self.sizes {
if let Size::Remainder { range } = size {
if avg_remainder_length < range.min {
remainder_length -= range.min;
num_remainders -= 1;
}
if let Size::Remainder { range } = size
&& avg_remainder_length < range.min
{
remainder_length -= range.min;
num_remainders -= 1;
}
}
if num_remainders > 0 {

View File

@@ -1,7 +1,7 @@
use crate::{
Size,
layout::{CellDirection, CellSize, StripLayout, StripLayoutFlags},
sizing::Sizing,
Size,
};
use egui::{Response, Ui};

View File

@@ -3,10 +3,8 @@
//! Turn on the `syntect` feature for great syntax highlighting of any language.
//! Otherwise, a very simple fallback will be used, that works okish for C, C++, Rust, and Python.
#![allow(clippy::mem_forget)] // False positive from enum_map macro
use egui::text::LayoutJob;
use egui::TextStyle;
use egui::text::LayoutJob;
/// View some code with syntax highlighting and selection.
pub fn code_view_ui(
@@ -28,17 +26,65 @@ pub fn highlight(
theme: &CodeTheme,
code: &str,
language: &str,
) -> LayoutJob {
highlight_inner(ctx, style, theme, code, language, None)
}
/// Add syntax highlighting to a code string, with custom `syntect` settings
///
/// The results are memoized, so you can call this every frame without performance penalty.
///
/// The `syntect` settings are memoized by *address*, so a stable reference should
/// be used to avoid unnecessary recomputation.
#[cfg(feature = "syntect")]
pub fn highlight_with(
ctx: &egui::Context,
style: &egui::Style,
theme: &CodeTheme,
code: &str,
language: &str,
settings: &SyntectSettings,
) -> LayoutJob {
highlight_inner(
ctx,
style,
theme,
code,
language,
Some(HighlightSettings(settings)),
)
}
fn highlight_inner(
ctx: &egui::Context,
style: &egui::Style,
theme: &CodeTheme,
code: &str,
language: &str,
settings: Option<HighlightSettings<'_>>,
) -> LayoutJob {
// We take in both context and style so that in situations where ui is not available such as when
// performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available
// performing it at a separate thread (ctx, ctx.global_style()) can be used and when ui is available
// (ui.ctx(), ui.style()) can be used
impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter {
#[expect(non_local_definitions)]
impl
egui::cache::ComputerMut<
(&egui::FontId, &CodeTheme, &str, &str, HighlightSettings<'_>),
LayoutJob,
> for Highlighter
{
fn compute(
&mut self,
(font_id, theme, code, lang): (&egui::FontId, &CodeTheme, &str, &str),
(font_id, theme, code, lang, settings): (
&egui::FontId,
&CodeTheme,
&str,
&str,
HighlightSettings<'_>,
),
) -> LayoutJob {
self.highlight(font_id.clone(), theme, code, lang)
Self::highlight(font_id.clone(), theme, code, lang, settings)
}
}
@@ -49,10 +95,28 @@ pub fn highlight(
.clone()
.unwrap_or_else(|| TextStyle::Monospace.resolve(style));
// Private type, so that users can't interfere with it in the `IdTypeMap`
#[cfg(feature = "syntect")]
#[derive(Clone, Default)]
struct PrivateSettings(std::sync::Arc<SyntectSettings>);
// Dummy private settings, to minimize code changes without `syntect`
#[cfg(not(feature = "syntect"))]
#[derive(Clone, Default)]
struct PrivateSettings(std::sync::Arc<()>);
ctx.memory_mut(|mem| {
let settings = settings.unwrap_or_else(|| {
HighlightSettings(
&mem.data
.get_temp_mut_or_default::<PrivateSettings>(egui::Id::NULL)
.0,
)
});
mem.caches
.cache::<HighlightCache>()
.get((&font_id, theme, code, language))
.get((&font_id, theme, code, language, settings))
.clone()
})
}
@@ -165,6 +229,10 @@ impl Default for CodeTheme {
}
impl CodeTheme {
pub fn is_dark(&self) -> bool {
self.dark_mode
}
/// Selects either dark or light theme based on the given style.
pub fn from_style(style: &egui::Style) -> Self {
let font_id = style
@@ -207,7 +275,7 @@ impl CodeTheme {
///
/// There is one dark and one light theme stored at any one time.
pub fn from_memory(ctx: &egui::Context, style: &egui::Style) -> Self {
#![allow(clippy::needless_return)]
#![expect(clippy::needless_return)]
let (id, default) = if style.visuals.dark_mode {
(egui::Id::new("dark"), Self::dark as fn(f32) -> Self)
@@ -236,7 +304,7 @@ impl CodeTheme {
///
/// There is one dark and one light theme stored at any one time.
pub fn store_in_memory(self, ctx: &egui::Context) {
let id = if ctx.style().visuals.dark_mode {
let id = if ctx.global_style().visuals.dark_mode {
egui::Id::new("dark")
} else {
egui::Id::new("light")
@@ -252,6 +320,24 @@ impl CodeTheme {
#[cfg(feature = "syntect")]
impl CodeTheme {
/// Change the font size
pub fn with_font_size(&self, font_size: f32) -> Self {
Self {
dark_mode: self.dark_mode,
syntect_theme: self.syntect_theme,
font_id: egui::FontId::monospace(font_size),
}
}
/// Change the `font_id` of the theme
pub fn with_font_id(&self, font_id: egui::FontId) -> Self {
Self {
dark_mode: self.dark_mode,
syntect_theme: self.syntect_theme,
font_id,
}
}
fn dark_with_font_id(font_id: egui::FontId) -> Self {
Self {
dark_mode: true,
@@ -270,12 +356,16 @@ impl CodeTheme {
/// Show UI for changing the color theme.
pub fn ui(&mut self, ui: &mut egui::Ui) {
egui::widgets::global_theme_preference_buttons(ui);
ui.horizontal(|ui| {
ui.selectable_value(&mut self.dark_mode, true, "🌙 Dark theme")
.on_hover_text("Use the dark mode theme");
for theme in SyntectTheme::all() {
if theme.is_dark() == self.dark_mode {
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
}
ui.selectable_value(&mut self.dark_mode, false, "☀ Light theme")
.on_hover_text("Use the light mode theme");
});
let current_theme_is_dark = self.is_dark();
for theme in SyntectTheme::all().filter(|t| t.is_dark() == current_theme_is_dark) {
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
}
}
}
@@ -284,8 +374,9 @@ 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 {
#![expect(clippy::mem_forget)]
use egui::{Color32, TextFormat};
Self {
dark_mode: true,
@@ -301,8 +392,9 @@ 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 {
#![expect(clippy::mem_forget)]
use egui::{Color32, TextFormat};
Self {
dark_mode: false,
@@ -333,12 +425,13 @@ impl CodeTheme {
ui.vertical(|ui| {
ui.set_width(150.0);
egui::widgets::global_theme_preference_buttons(ui);
ui.add_space(8.0);
ui.separator();
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.selectable_value(&mut self.dark_mode, true, "🌙 Dark theme")
.on_hover_text("Use the dark mode theme");
ui.selectable_value(&mut self.dark_mode, false, "☀ Light theme")
.on_hover_text("Use the light mode theme");
});
ui.scope(|ui| {
for (tt, tt_name) in [
(TokenType::Comment, "// comment"),
@@ -395,13 +488,14 @@ impl CodeTheme {
// ----------------------------------------------------------------------------
#[cfg(feature = "syntect")]
struct Highlighter {
ps: syntect::parsing::SyntaxSet,
ts: syntect::highlighting::ThemeSet,
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SyntectSettings {
pub ps: syntect::parsing::SyntaxSet,
pub ts: syntect::highlighting::ThemeSet,
}
#[cfg(feature = "syntect")]
impl Default for Highlighter {
impl Default for SyntectSettings {
fn default() -> Self {
profiling::function_scope!();
Self {
@@ -411,16 +505,33 @@ impl Default for Highlighter {
}
}
/// Highlight settings are memoized by reference address, rather than value
#[cfg(feature = "syntect")]
#[derive(Copy, Clone)]
struct HighlightSettings<'a>(&'a SyntectSettings);
#[cfg(not(feature = "syntect"))]
#[derive(Copy, Clone)]
struct HighlightSettings<'a>(&'a ());
impl std::hash::Hash for HighlightSettings<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(self.0, state);
}
}
#[derive(Default)]
struct Highlighter;
impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(
&self,
font_id: egui::FontId,
theme: &CodeTheme,
code: &str,
lang: &str,
settings: HighlightSettings<'_>,
) -> LayoutJob {
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
Self::highlight_impl(theme, code, lang, settings).unwrap_or_else(|| {
// Fallback:
LayoutJob::simple(
code.into(),
@@ -436,19 +547,25 @@ impl Highlighter {
}
#[cfg(feature = "syntect")]
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
fn highlight_impl(
theme: &CodeTheme,
text: &str,
language: &str,
highlighter: HighlightSettings<'_>,
) -> Option<LayoutJob> {
profiling::function_scope!();
use syntect::easy::HighlightLines;
use syntect::highlighting::FontStyle;
use syntect::util::LinesWithEndings;
let syntax = self
let syntax = highlighter
.0
.ps
.find_syntax_by_name(language)
.or_else(|| self.ps.find_syntax_by_extension(language))?;
.or_else(|| highlighter.0.ps.find_syntax_by_extension(language))?;
let syn_theme = theme.syntect_theme.syntect_key_name();
let mut h = HighlightLines::new(syntax, &self.ts.themes[syn_theme]);
let mut h = HighlightLines::new(syntax, &highlighter.0.ts.themes[syn_theme]);
use egui::text::{LayoutSection, TextFormat};
@@ -458,7 +575,7 @@ impl Highlighter {
};
for line in LinesWithEndings::from(text) {
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
for (style, range) in h.highlight_line(line, &highlighter.0.ps).ok()? {
let fg = style.foreground;
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
let italics = style.font_style.contains(FontStyle::ITALIC);
@@ -505,18 +622,13 @@ fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
// ----------------------------------------------------------------------------
#[cfg(not(feature = "syntect"))]
#[derive(Default)]
struct Highlighter {}
#[cfg(not(feature = "syntect"))]
impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight_impl(
&self,
theme: &CodeTheme,
mut text: &str,
language: &str,
_settings: HighlightSettings<'_>,
) -> Option<LayoutJob> {
profiling::function_scope!();

View File

@@ -4,13 +4,13 @@
//! Takes all available height, so if you want something below the table, put it in a strip.
use egui::{
scroll_area::{ScrollAreaOutput, ScrollBarVisibility},
Align, Id, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2, Vec2b,
scroll_area::{ScrollAreaOutput, ScrollBarVisibility, ScrollSource},
};
use crate::{
layout::{CellDirection, CellSize, StripLayoutFlags},
StripLayout,
layout::{CellDirection, CellSize, StripLayoutFlags},
};
// -----------------------------------------------------------------=----------
@@ -472,14 +472,14 @@ impl<'a> TableBuilder<'a> {
for (i, column) in columns.iter_mut().enumerate() {
let column_resize_id = ui.id().with("resize_column").with(i);
if let Some(response) = ui.ctx().read_response(column_resize_id) {
if response.double_clicked() {
column.auto_size_this_frame = true;
}
if let Some(response) = ui.ctx().read_response(column_resize_id)
&& response.double_clicked()
{
column.auto_size_this_frame = true;
}
}
let striped = striped.unwrap_or(ui.visuals().striped);
let striped = striped.unwrap_or_else(|| ui.visuals().striped);
let state_id = ui.id().with(id_salt);
@@ -507,6 +507,7 @@ impl<'a> TableBuilder<'a> {
striped: false,
hovered: false,
selected: false,
overline: false,
response: &mut response,
});
layout.allocate_rect();
@@ -547,7 +548,7 @@ impl<'a> TableBuilder<'a> {
sense,
} = self;
let striped = striped.unwrap_or(ui.visuals().striped);
let striped = striped.unwrap_or_else(|| ui.visuals().striped);
let state_id = ui.id().with(id_salt);
@@ -655,7 +656,7 @@ impl TableState {
}
fn store(self, ui: &egui::Ui, state_id: egui::Id) {
#![allow(clippy::needless_return)]
#![expect(clippy::needless_return)]
#[cfg(feature = "serde")]
{
return ui.data_mut(|d| d.insert_persisted(state_id, self));
@@ -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)
@@ -847,7 +851,7 @@ impl Table<'_> {
if column.is_auto() && (is_sizing_pass || !column_is_resizable) {
*column_width = width_range.clamp(max_used_widths[i]);
} else if column_is_resizable {
let column_resize_id = ui.id().with("resize_column").with(i);
let column_resize_id = state_id.with("resize_column").with(i);
let mut p0 = egui::pos2(x, table_top);
let mut p1 = egui::pos2(x, bottom);
@@ -860,27 +864,27 @@ impl Table<'_> {
if column.auto_size_this_frame {
// Auto-size: resize to what is needed.
*column_width = width_range.clamp(max_used_widths[i]);
} else if resize_response.dragged() {
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let mut new_width = *column_width + pointer.x - x;
if !column.clip {
// Unless we clip we don't want to shrink below the
// size that was actually used.
// However, we still want to allow content that shrinks when you try
// to make the column less wide, so we allow some small shrinkage each frame:
// big enough to allow shrinking over time, small enough not to look ugly when
// shrinking fails. This is a bit of a HACK around immediate mode.
let max_shrinkage_per_frame = 8.0;
new_width =
new_width.at_least(max_used_widths[i] - max_shrinkage_per_frame);
}
new_width = width_range.clamp(new_width);
let x = x - *column_width + new_width;
(p0.x, p1.x) = (x, x);
*column_width = new_width;
} else if resize_response.dragged()
&& let Some(pointer) = ui.ctx().pointer_latest_pos()
{
let mut new_width = *column_width + pointer.x - x;
if !column.clip {
// Unless we clip we don't want to shrink below the
// size that was actually used.
// However, we still want to allow content that shrinks when you try
// to make the column less wide, so we allow some small shrinkage each frame:
// big enough to allow shrinking over time, small enough not to look ugly when
// shrinking fails. This is a bit of a HACK around immediate mode.
let max_shrinkage_per_frame = 8.0;
new_width =
new_width.at_least(max_used_widths[i] - max_shrinkage_per_frame);
}
new_width = width_range.clamp(new_width);
let x = x - *column_width + new_width;
(p0.x, p1.x) = (x, x);
*column_width = new_width;
}
let dragging_something_else =
@@ -888,7 +892,7 @@ impl Table<'_> {
let resize_hover = resize_response.hovered() && !dragging_something_else;
if resize_hover || resize_response.dragged() {
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeColumn);
ui.set_cursor_icon(egui::CursorIcon::ResizeColumn);
}
let stroke = if resize_response.dragged() {
@@ -987,9 +991,10 @@ impl<'a> TableBody<'a> {
row_index: self.row_index,
col_index: 0,
height,
striped: self.striped && self.row_index % 2 == 0,
striped: self.striped && self.row_index.is_multiple_of(2),
hovered: self.hovered_row_index == Some(self.row_index),
selected: false,
overline: false,
response: &mut response,
});
self.capture_hover_state(&response, self.row_index);
@@ -1068,9 +1073,10 @@ impl<'a> TableBody<'a> {
row_index,
col_index: 0,
height: row_height_sans_spacing,
striped: self.striped && (row_index + self.row_index) % 2 == 0,
striped: self.striped && (row_index + self.row_index).is_multiple_of(2),
hovered: self.hovered_row_index == Some(row_index),
selected: false,
overline: false,
response: &mut response,
});
self.capture_hover_state(&response, row_index);
@@ -1149,9 +1155,10 @@ impl<'a> TableBody<'a> {
row_index,
col_index: 0,
height: row_height,
striped: self.striped && (row_index + self.row_index) % 2 == 0,
striped: self.striped && (row_index + self.row_index).is_multiple_of(2),
hovered: self.hovered_row_index == Some(row_index),
selected: false,
overline: false,
response: &mut response,
});
self.capture_hover_state(&response, row_index);
@@ -1171,8 +1178,9 @@ impl<'a> TableBody<'a> {
row_index,
col_index: 0,
height: row_height,
striped: self.striped && (row_index + self.row_index) % 2 == 0,
striped: self.striped && (row_index + self.row_index).is_multiple_of(2),
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(),
};
@@ -1315,7 +1325,7 @@ impl TableRow<'_, '_> {
*self.response = Some(
self.response
.as_ref()
.map_or(response.clone(), |r| r.union(response.clone())),
.map_or_else(|| response.clone(), |r| r.union(response.clone())),
);
(used_rect, response)
@@ -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.