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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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};
|
||||
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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
Size,
|
||||
layout::{CellDirection, CellSize, StripLayout, StripLayoutFlags},
|
||||
sizing::Sizing,
|
||||
Size,
|
||||
};
|
||||
use egui::{Response, Ui};
|
||||
|
||||
|
||||
@@ -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!();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user