diff --git a/Cargo.lock b/Cargo.lock index ae896877bdc..af293f56a78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1011,14 +1011,30 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.16" +name = "crossbeam-deque" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1460,6 +1476,8 @@ dependencies = [ "log", "nohash-hasher", "parking_lot", + "puffin", + "rayon", "serde", ] @@ -3000,6 +3018,26 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rctree" version = "0.5.0" diff --git a/README.md b/README.md index 0ff209dda7a..17f918836b4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ ui.horizontal(|ui| { ui.text_edit_singleline(&mut name); }); ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); -if ui.button("Click each year").clicked() { +if ui.button("Increment").clicked() { age += 1; } ui.label(format!("Hello '{name}', age {age}")); @@ -376,21 +376,21 @@ The library was originally called "Emigui", but was renamed to "egui" in 2020. ## Credits -egui author and maintainer: Emil Ernerfeldt [(@emilk](https://github.com/emilk)). +egui author and maintainer: Emil Ernerfeldt ([@emilk](https://github.com/emilk)). Notable contributions by: -* [@n2](https://github.com/n2): [Mobile web input and IME support](https://github.com/emilk/egui/pull/253). -* [@optozorax](https://github.com/optozorax): [Arbitrary widget data storage](https://github.com/emilk/egui/pull/257). -* [@quadruple-output](https://github.com/quadruple-output): [Multitouch](https://github.com/emilk/egui/pull/306). -* [@EmbersArc](https://github.com/EmbersArc): [Plots](https://github.com/emilk/egui/pulls?q=+is%3Apr+author%3AEmbersArc). -* [@AsmPrgmC3](https://github.com/AsmPrgmC3): [Proper sRGBA blending for web](https://github.com/emilk/egui/pull/650). -* [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685). -* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543). -* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868). -* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050). -* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625). -* [@mwcampbell](https://github.com/mwcampbell): [AccessKit](https://github.com/AccessKit/accesskit) [integration](https://github.com/emilk/egui/pull/2294). +* [@n2](https://github.com/n2): [Mobile web input and IME support](https://github.com/emilk/egui/pull/253) +* [@optozorax](https://github.com/optozorax): [Arbitrary widget data storage](https://github.com/emilk/egui/pull/257) +* [@quadruple-output](https://github.com/quadruple-output): [Multitouch](https://github.com/emilk/egui/pull/306) +* [@EmbersArc](https://github.com/EmbersArc): [Plots](https://github.com/emilk/egui/pulls?q=+is%3Apr+author%3AEmbersArc) +* [@AsmPrgmC3](https://github.com/AsmPrgmC3): [Proper sRGBA blending for web](https://github.com/emilk/egui/pull/650) +* [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685) +* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543) +* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868) +* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050) +* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625) +* [@mwcampbell](https://github.com/mwcampbell): [AccessKit](https://github.com/AccessKit/accesskit) [integration](https://github.com/emilk/egui/pull/2294) * [@hasenbanck](https://github.com/hasenbanck), [@s-nie](https://github.com/s-nie), [@Wumpf](https://github.com/Wumpf): [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) * [@jprochazk](https://github.com/jprochazk): [egui image API](https://github.com/emilk/egui/issues/3291) * And [many more](https://github.com/emilk/egui/graphs/contributors?type=a). diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index bdc1f84df61..a67cec27773 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -88,7 +88,11 @@ puffin = [ ] ## Enables wayland support and fixes clipboard issue. -wayland = ["egui-winit/wayland"] +wayland = [ + "egui-winit/wayland", + "egui-wgpu?/wayland", + "egui_glow?/wayland", +] ## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web. ## @@ -114,7 +118,11 @@ web_screen_reader = [ wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"] ## Enables compiling for x11. -x11 = ["egui-winit/x11"] +x11 = [ + "egui-winit/x11", + "egui-wgpu?/x11", + "egui_glow?/x11", +] ## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit. ## This is used to generate images for examples. diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 3bc480c65ed..d34d665b299 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -293,7 +293,7 @@ pub fn run_native( /// .labelled_by(name_label.id); /// }); /// ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); -/// if ui.button("Click each year").clicked() { +/// if ui.button("Increment").clicked() { /// age += 1; /// } /// ui.label(format!("Hello '{name}', age {age}")); diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index b653238e82f..d05f530fa54 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -31,9 +31,15 @@ all-features = true ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. puffin = ["dep:puffin"] -## Enable [`winit`](https://docs.rs/winit) integration. +## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11` winit = ["dep:winit"] +## Enables Wayland support for winit. +wayland = ["winit?/wayland"] + +## Enables x11 support for winit. +x11 = ["winit?/x11"] + [dependencies] egui = { version = "0.25.0", path = "../egui", default-features = false } diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 780c90866ac..21227ef9058 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -814,7 +814,7 @@ impl Renderer { }; if index_count > 0 { - crate::profile_scope!("indices"); + crate::profile_scope!("indices", index_count.to_string()); self.index_buffer.slices.clear(); let required_index_buffer_size = (std::mem::size_of::() * index_count) as u64; @@ -848,7 +848,7 @@ impl Renderer { } } if vertex_count > 0 { - crate::profile_scope!("vertices"); + crate::profile_scope!("vertices", vertex_count.to_string()); self.vertex_buffer.slices.clear(); let required_vertex_buffer_size = (std::mem::size_of::() * vertex_count) as u64; diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 993a6153331..c6d5e9e5a15 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -66,7 +66,12 @@ persistence = ["serde", "epaint/serde", "ron"] ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. ## ## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin"] +puffin = ["dep:puffin", "epaint/puffin"] + +## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). +## +## This can help performance for graphics-intense applications. +rayon = ["epaint/rayon"] ## Allow serialization using [`serde`](https://docs.rs/serde). serde = ["dep:serde", "epaint/serde", "accesskit?/serde"] diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index b9a233273aa..09061e877c3 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -178,12 +178,50 @@ impl ContextImpl { /// Used to store each widgets [Id], [Rect] and [Sense] each frame. /// Used to check for overlaps between widgets when handling events. -struct WidgetRect { - id: Id, - rect: Rect, - sense: Sense, +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WidgetRect { + /// Where the widget is. + pub rect: Rect, + + /// The globally unique widget id. + /// + /// For interactive widgets, this better be globally unique. + /// If not there will get weird bugs, + /// and also big red warning test on the screen in debug builds + /// (see [`Options::warn_on_id_clash`]). + /// + /// You can ensure globally unique ids using [`Ui::push_id`]. + pub id: Id, + + /// How the widget responds to interaction. + pub sense: Sense, +} + +/// Stores the positions of all widgets generated during a single egui update/frame. +/// +/// Acgtually, only those that are on screen. +#[derive(Default, Clone, PartialEq, Eq)] +pub struct WidgetRects { + /// All widgets, in painting order. + pub by_layer: HashMap>, +} + +impl WidgetRects { + /// Clear the contents while retaining allocated memory. + pub fn clear(&mut self) { + for rects in self.by_layer.values_mut() { + rects.clear(); + } + } + + /// Insert the given widget rect in the given layer. + pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) { + self.by_layer.entry(layer_id).or_default().push(widget_rect); + } } +// ---------------------------------------------------------------------------- + /// State stored per viewport #[derive(Default)] struct ViewportState { @@ -210,10 +248,10 @@ struct ViewportState { used: bool, /// Written to during the frame. - layer_rects_this_frame: HashMap>, + layer_rects_this_frame: WidgetRects, /// Read - layer_rects_prev_frame: HashMap>, + layer_rects_prev_frame: WidgetRects, /// State related to repaint scheduling. repaint: ViewportRepaintInfo, @@ -362,14 +400,6 @@ impl ContextImpl { .native_pixels_per_point .unwrap_or(1.0); - { - std::mem::swap( - &mut viewport.layer_rects_prev_frame, - &mut viewport.layer_rects_this_frame, - ); - viewport.layer_rects_this_frame.clear(); - } - let all_viewport_ids: ViewportIdSet = self.all_viewport_ids(); let viewport = self.viewports.entry(self.viewport_id()).or_default(); @@ -609,12 +639,12 @@ impl Default for Context { } impl Context { - // Do read-only (shared access) transaction on Context + /// Do read-only (shared access) transaction on Context fn read(&self, reader: impl FnOnce(&ContextImpl) -> R) -> R { reader(&self.0.read()) } - // Do read-write (exclusive access) transaction on Context + /// Do read-write (exclusive access) transaction on Context fn write(&self, writer: impl FnOnce(&mut ContextImpl) -> R) -> R { writer(&mut self.0.write()) } @@ -845,19 +875,21 @@ impl Context { // it is ok to reuse the same ID for e.g. a frame around a widget, // or to check for interaction with the same widget twice: - if prev_rect.expand(0.1).contains_rect(new_rect) - || new_rect.expand(0.1).contains_rect(prev_rect) - { + let is_same_rect = prev_rect.expand(0.1).contains_rect(new_rect) + || new_rect.expand(0.1).contains_rect(prev_rect); + if is_same_rect { return; } let show_error = |widget_rect: Rect, text: String| { + let screen_rect = self.screen_rect(); + let text = format!("🔥 {text}"); let color = self.style().visuals.error_fg_color; let painter = self.debug_painter(); painter.rect_stroke(widget_rect, 0.0, (1.0, color)); - let below = widget_rect.bottom() + 32.0 < self.input(|i| i.screen_rect.bottom()); + let below = widget_rect.bottom() + 32.0 < screen_rect.bottom(); let text_rect = if below { painter.debug_text( @@ -1784,7 +1816,24 @@ impl ContextImpl { .graphics .drain(self.memory.areas().order(), &self.memory.layer_transforms); - if viewport.input.wants_repaint() { + let mut repaint_needed = false; + + { + if self.memory.options.repaint_on_widget_change { + crate::profile_function!("compare-widget-rects"); + if viewport.layer_rects_prev_frame != viewport.layer_rects_this_frame { + repaint_needed = true; // Some widget has moved + } + } + + std::mem::swap( + &mut viewport.layer_rects_prev_frame, + &mut viewport.layer_rects_this_frame, + ); + viewport.layer_rects_this_frame.clear(); + } + + if repaint_needed || viewport.input.wants_repaint() { self.request_repaint(ended_viewport_id); } @@ -1932,13 +1981,13 @@ impl Context { let paint_stats = PaintStats::from_shapes(&shapes); let clipped_primitives = { crate::profile_scope!("tessellator::tessellate_shapes"); - tessellator::tessellate_shapes( + tessellator::Tessellator::new( pixels_per_point, tessellation_options, font_tex_size, prepared_discs, - shapes, ) + .tessellate_shapes(shapes) }; ctx.paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives); clipped_primitives @@ -2110,6 +2159,8 @@ impl Context { /// /// Will return false if some other area is covering the given layer. /// + /// The given rectangle is assumed to have been clipped by its parent clip rect. + /// /// See also [`Response::contains_pointer`]. pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { let rect = @@ -2145,6 +2196,8 @@ impl Context { /// If another widget is covering us and is listening for the same input (click and/or drag), /// this will return false. /// + /// The given rectangle is assumed to have been clipped by its parent clip rect. + /// /// See also [`Response::contains_pointer`]. pub fn widget_contains_pointer( &self, @@ -2153,6 +2206,10 @@ impl Context { sense: Sense, rect: Rect, ) -> bool { + if !rect.is_positive() { + return false; // don't even remember this widget + } + let contains_pointer = self.rect_contains_pointer(layer_id, rect); let mut blocking_widget = None; @@ -2168,12 +2225,10 @@ impl Context { // We add all widgets here, even non-interactive ones, // because we need this list not only for checking for blocking widgets, - // but also to know when we have reach the widget we are checking for cover. + // but also to know when we have reached the widget we are checking for cover. viewport .layer_rects_this_frame - .entry(layer_id) - .or_default() - .push(WidgetRect { id, rect, sense }); + .insert(layer_id, WidgetRect { id, rect, sense }); // Check if any other widget is covering us. // Whichever widget is added LAST (=on top) gets the input. @@ -2182,7 +2237,7 @@ impl Context { if let Some(pointer_pos) = pointer_pos { // Apply the inverse transformation of this layer to the pointer pos. let pointer_pos = transform.inverse() * pointer_pos; - if let Some(rects) = viewport.layer_rects_prev_frame.get(&layer_id) { + if let Some(rects) = viewport.layer_rects_prev_frame.by_layer.get(&layer_id) { for blocking in rects.iter().rev() { if blocking.id == id { // There are no earlier widgets before this one, @@ -2317,25 +2372,14 @@ impl Context { impl Context { /// Show a ui for settings (style and tessellation options). pub fn settings_ui(&self, ui: &mut Ui) { - use crate::containers::*; + let prev_options = self.options(|o| o.clone()); + let mut options = prev_options.clone(); - CollapsingHeader::new("🎑 Style") - .default_open(true) - .show(ui, |ui| { - self.style_ui(ui); - }); + options.ui(ui); - CollapsingHeader::new("✒ Painting") - .default_open(true) - .show(ui, |ui| { - let prev_tessellation_options = self.tessellation_options(|o| *o); - let mut tessellation_options = prev_tessellation_options; - tessellation_options.ui(ui); - ui.vertical_centered(|ui| reset_button(ui, &mut tessellation_options)); - if tessellation_options != prev_tessellation_options { - self.tessellation_options_mut(move |o| *o = tessellation_options); - } - }); + if options != prev_options { + self.options_mut(move |o| *o = options); + } } /// Show the state of egui, including its input and output. diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 181d30feda2..1cd96faec5f 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -1,5 +1,7 @@ // TODO(emilk): have separate types `PositionId` and `UniqueId`. ? +use std::num::NonZeroU64; + /// egui tracks widgets frame-to-frame using [`Id`]s. /// /// For instance, if you start dragging a slider one frame, egui stores @@ -25,9 +27,11 @@ /// /// Then there are widgets that need no identifiers at all, like labels, /// because they have no state nor are interacted with. +/// +/// This is niche-optimized to that `Option` is the same size as `Id`. #[derive(Clone, Copy, Hash, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Id(u64); +pub struct Id(NonZeroU64); impl Id { /// A special [`Id`], in particular as a key to [`crate::Memory::data`] @@ -35,39 +39,47 @@ impl Id { /// /// The null [`Id`] is still a valid id to use in all circumstances, /// though obviously it will lead to a lot of collisions if you do use it! - pub const NULL: Self = Self(0); + pub const NULL: Self = Self(NonZeroU64::MAX); - pub(crate) const fn background() -> Self { - Self(1) + #[inline] + const fn from_hash(hash: u64) -> Self { + if let Some(nonzero) = NonZeroU64::new(hash) { + Self(nonzero) + } else { + Self(NonZeroU64::MIN) // The hash was exactly zero (very bad luck) + } } /// Generate a new [`Id`] by hashing some source (e.g. a string or integer). pub fn new(source: impl std::hash::Hash) -> Self { - Self(epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source)) + Self::from_hash(epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source)) } /// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument. pub fn with(self, child: impl std::hash::Hash) -> Self { use std::hash::{BuildHasher, Hasher}; let mut hasher = epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher(); - hasher.write_u64(self.0); + hasher.write_u64(self.0.get()); child.hash(&mut hasher); - Self(hasher.finish()) + Self::from_hash(hasher.finish()) } /// Short and readable summary pub fn short_debug_format(&self) -> String { - format!("{:04X}", self.0 as u16) + format!("{:04X}", self.value() as u16) } + /// The inner value of the [`Id`]. + /// + /// This is a high-entropy hash, or [`Self::NULL`]. #[inline(always)] - pub(crate) fn value(&self) -> u64 { - self.0 + pub fn value(&self) -> u64 { + self.0.get() } #[cfg(feature = "accesskit")] pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { - self.0.into() + self.value().into() } } @@ -92,6 +104,12 @@ impl From for Id { } } +#[test] +fn id_size() { + assert_eq!(std::mem::size_of::(), 8); + assert_eq!(std::mem::size_of::>(), 8); +} + // ---------------------------------------------------------------------------- // Idea taken from the `nohash_hasher` crate. diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 6f0cada7807..43b3a88b81f 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -146,6 +146,8 @@ impl Widget for &mut epaint::TessellationOptions { debug_ignore_clip_rects, bezier_tolerance, epsilon: _, + parallel_tessellation, + validate_meshes, } = self; ui.checkbox(feathering, "Feathering (antialias)") @@ -176,6 +178,12 @@ impl Widget for &mut epaint::TessellationOptions { ui.checkbox(debug_paint_clip_rects, "Paint clip rectangles"); ui.checkbox(debug_paint_text_rects, "Paint text bounds"); }); + + ui.add_enabled(epaint::HAS_RAYON, crate::Checkbox::new(parallel_tessellation, "Parallelize tessellation") + ).on_hover_text("Only available if epaint was compiled with the rayon feature") + .on_disabled_hover_text("epaint was not compiled with the rayon feature"); + + ui.checkbox(validate_meshes, "Validate meshes").on_hover_text("Check that incoming meshes are valid, i.e. that all indices are in range, etc."); }) .response } diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 4bdcb968033..2c97017106f 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -90,7 +90,7 @@ impl LayerId { pub fn background() -> Self { Self { order: Order::Background, - id: Id::background(), + id: Id::new("background"), } } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 67cfacc2c31..cdf198751ce 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -410,7 +410,7 @@ pub mod text { pub use { containers::*, - context::{Context, RequestRepaintInfo}, + context::{Context, RequestRepaintInfo, WidgetRect, WidgetRects}, data::{ input::*, output::{ diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index e75cff2bec6..1a39c2a2c8b 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -161,7 +161,7 @@ impl FocusDirection { // ---------------------------------------------------------------------------- /// Some global options that you can read and write. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Options { @@ -191,6 +191,11 @@ pub struct Options { /// Controls the tessellator. pub tessellation_options: epaint::TessellationOptions, + /// If any widget moves or changes id, repaint everything. + /// + /// This is `true` by default. + pub repaint_on_widget_change: bool, + /// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud. /// /// The only change to egui is that labels can be focused by pressing tab. @@ -223,6 +228,7 @@ impl Default for Options { zoom_factor: 1.0, zoom_with_keyboard: true, tessellation_options: Default::default(), + repaint_on_widget_change: true, screen_reader: false, preload_font_glyphs: true, warn_on_id_clash: cfg!(debug_assertions), @@ -230,6 +236,56 @@ impl Default for Options { } } +impl Options { + /// Show the options in the ui. + pub fn ui(&mut self, ui: &mut crate::Ui) { + let Self { + style, // covered above + zoom_factor: _, // TODO + zoom_with_keyboard, + tessellation_options, + repaint_on_widget_change, + screen_reader: _, // needs to come from the integration + preload_font_glyphs: _, + warn_on_id_clash, + } = self; + + use crate::Widget as _; + + CollapsingHeader::new("⚙ Options") + .default_open(false) + .show(ui, |ui| { + ui.checkbox( + repaint_on_widget_change, + "Repaint if any widget moves or changes id", + ); + + ui.checkbox( + zoom_with_keyboard, + "Zoom with keyboard (Cmd +, Cmd -, Cmd 0)", + ); + + ui.checkbox(warn_on_id_clash, "Warn if two widgets have the same Id"); + }); + + use crate::containers::*; + CollapsingHeader::new("🎑 Style") + .default_open(true) + .show(ui, |ui| { + std::sync::Arc::make_mut(style).ui(ui); + }); + + CollapsingHeader::new("✒ Painting") + .default_open(true) + .show(ui, |ui| { + tessellation_options.ui(ui); + ui.vertical_centered(|ui| crate::reset_button(ui, tessellation_options)); + }); + + ui.vertical_centered(|ui| crate::reset_button(ui, self)); + } +} + // ---------------------------------------------------------------------------- /// Say there is a button in a scroll area. diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index c0eb8e12fca..e318fbcd621 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -6,7 +6,7 @@ use crate::{ Color32, Context, FontId, }; use epaint::{ - text::{Fonts, Galley}, + text::{Fonts, Galley, LayoutJob}, CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke, }; @@ -436,9 +436,18 @@ impl Painter { self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY)) } + /// Lay out this text layut job in a galley. + /// + /// Paint the results with [`Self::galley`]. + #[inline] + #[must_use] + pub fn layout_job(&self, layout_job: LayoutJob) -> Arc { + self.fonts(|f| f.layout_job(layout_job)) + } + /// Paint text that has already been laid out in a [`Galley`]. /// - /// You can create the [`Galley`] with [`Self::layout`]. + /// You can create the [`Galley`] with [`Self::layout`] or [`Self::layout_job`]. /// /// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color. /// diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 027938fb18c..bc252052ae4 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -770,9 +770,8 @@ impl Response { /// ``` /// /// See also: [`Ui::menu_button`] and [`Ui::close_menu`]. - pub fn context_menu(self, add_contents: impl FnOnce(&mut Ui)) -> Self { - menu::context_menu(&self, add_contents); - self + pub fn context_menu(&self, add_contents: impl FnOnce(&mut Ui)) -> Option> { + menu::context_menu(self, add_contents) } } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 46531f9c835..0cf9c5f6243 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -455,7 +455,7 @@ impl ScrollStyle { pub fn thin() -> Self { Self { floating: true, - bar_width: 12.0, + bar_width: 10.0, floating_allocated_width: 6.0, foreground_color: false, @@ -479,7 +479,7 @@ impl ScrollStyle { pub fn floating() -> Self { Self { floating: true, - bar_width: 12.0, + bar_width: 10.0, foreground_color: true, floating_allocated_width: 0.0, dormant_background_opacity: 0.0, diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 9a5123f7d87..cce7bd41e7c 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -30,6 +30,7 @@ use crate::{ /// ``` pub struct Ui { /// ID of this ui. + /// /// Generated based on id of parent ui together with /// another source of child identity (e.g. window title). /// Acts like a namespace for child uis. @@ -38,6 +39,7 @@ pub struct Ui { id: Id, /// This is used to create a unique interact ID for some widgets. + /// /// This value is based on where in the hierarchy of widgets this Ui is in, /// and the value is increment with each added child widget. /// This works as an Id source only as long as new widgets aren't added or removed. @@ -99,7 +101,6 @@ impl Ui { crate::egui_assert!(!max_rect.any_nan()); let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value(); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); - let menu_state = self.menu_state(); Ui { id: self.id.with(id_source), next_auto_id_source, @@ -107,7 +108,7 @@ impl Ui { style: self.style.clone(), placer: Placer::new(max_rect, layout), enabled: self.enabled, - menu_state, + menu_state: self.menu_state.clone(), } } @@ -2233,10 +2234,6 @@ impl Ui { self.menu_state = None; } - pub(crate) fn menu_state(&self) -> Option>> { - self.menu_state.clone() - } - pub(crate) fn set_menu_state(&mut self, menu_state: Option>>) { self.menu_state = menu_state; } diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index b5ae6545800..09d3b9f3d7d 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -79,6 +79,8 @@ impl<'a> DragValue<'a> { } /// How much the value changes when dragged one point (logical pixel). + /// + /// Should be finite and greater than zero. #[inline] pub fn speed(mut self, speed: impl Into) -> Self { self.speed = speed.into(); diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index b68fb4337e1..f5bb7ac425a 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -130,11 +130,12 @@ impl Label { // we prioritize touch-scrolling: let allow_drag_to_select = ui.input(|i| !i.any_touches()); - let select_sense = if allow_drag_to_select { + let mut select_sense = if allow_drag_to_select { Sense::click_and_drag() } else { Sense::click() }; + select_sense.focusable = false; // Don't move focus to labels with TAB key. sense = sense.union(select_sense); } diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 8bf3da5f501..e15a11b62d7 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -30,6 +30,8 @@ syntect = ["egui_demo_lib/syntect"] glow = ["eframe/glow"] wgpu = ["eframe/wgpu", "bytemuck", "dep:wgpu"] +wayland = ["eframe/wayland"] +x11 = ["eframe/x11"] [dependencies] chrono = { version = "0.4", default-features = false, features = [ diff --git a/crates/egui_demo_lib/src/demo/code_example.rs b/crates/egui_demo_lib/src/demo/code_example.rs index 7edb67020ae..b4adf8fb50c 100644 --- a/crates/egui_demo_lib/src/demo/code_example.rs +++ b/crates/egui_demo_lib/src/demo/code_example.rs @@ -45,11 +45,11 @@ impl CodeExample { show_code( ui, r#" - if ui.button("Click each year").clicked() { + if ui.button("Increment").clicked() { self.age += 1; }"#, ); - if ui.button("Click each year").clicked() { + if ui.button("Increment").clicked() { self.age += 1; } ui.end_row(); diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index edd5215bfb5..f497696ee18 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -791,8 +791,28 @@ impl InteractionDemo { let PlotResponse { response, inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered), + hovered_plot_item, .. } = plot.show(ui, |plot_ui| { + plot_ui.line( + Line::new(PlotPoints::from_explicit_callback( + move |x| x.sin(), + .., + 100, + )) + .color(Color32::RED) + .id(egui::Id::new("sin")), + ); + plot_ui.line( + Line::new(PlotPoints::from_explicit_callback( + move |x| x.cos(), + .., + 100, + )) + .color(Color32::BLUE) + .id(egui::Id::new("cos")), + ); + ( plot_ui.screen_from_plot(PlotPoint::new(0.0, 0.0)), plot_ui.pointer_coordinate(), @@ -824,6 +844,15 @@ impl InteractionDemo { ); ui.label(format!("pointer coordinate drag delta: {coordinate_text}")); + let hovered_item = if hovered_plot_item == Some(egui::Id::new("sin")) { + "red sin" + } else if hovered_plot_item == Some(egui::Id::new("cos")) { + "blue cos" + } else { + "none" + }; + ui.label(format!("hovered plot item: {hovered_item}")); + response } } diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index 5174ee30161..e4ed23cd9a8 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -39,9 +39,15 @@ links = ["egui-winit?/links"] ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. puffin = ["dep:puffin", "egui-winit?/puffin", "egui/puffin"] -## Enable [`winit`](https://docs.rs/winit) integration. +## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11` winit = ["egui-winit", "dep:winit"] +## Enables Wayland support for winit. +wayland = ["winit?/wayland"] + +## Enables x11 support for winit. +x11 = ["winit?/x11"] + [dependencies] egui = { version = "0.25.0", path = "../egui", default-features = false, features = [ diff --git a/crates/egui_plot/src/axis.rs b/crates/egui_plot/src/axis.rs index d81debed322..df182631c1b 100644 --- a/crates/egui_plot/src/axis.rs +++ b/crates/egui_plot/src/axis.rs @@ -1,9 +1,9 @@ use std::{fmt::Debug, ops::RangeInclusive, sync::Arc}; use egui::{ - emath::{remap_clamp, round_to_decimals}, + emath::{remap_clamp, round_to_decimals, Rot2}, epaint::TextShape, - Pos2, Rangef, Rect, Response, Sense, Shape, TextStyle, Ui, WidgetText, + Pos2, Rangef, Rect, Response, Sense, TextStyle, Ui, Vec2, WidgetText, }; use super::{transform::PlotTransform, GridMark}; @@ -64,6 +64,16 @@ impl From for Placement { } } +impl From for HPlacement { + #[inline] + fn from(placement: Placement) -> Self { + match placement { + Placement::LeftBottom => Self::Left, + Placement::RightTop => Self::Right, + } + } +} + impl From for Placement { #[inline] fn from(placement: VPlacement) -> Self { @@ -74,6 +84,16 @@ impl From for Placement { } } +impl From for VPlacement { + #[inline] + fn from(placement: Placement) -> Self { + match placement { + Placement::LeftBottom => Self::Bottom, + Placement::RightTop => Self::Top, + } + } +} + /// Axis configuration. /// /// Used to configure axis label and ticks. @@ -211,16 +231,18 @@ impl AxisHints { #[derive(Clone)] pub(super) struct AxisWidget { - pub(super) range: RangeInclusive, - pub(super) hints: AxisHints, - pub(super) rect: Rect, - pub(super) transform: Option, - pub(super) steps: Arc>, + pub range: RangeInclusive, + pub hints: AxisHints, + + /// The region where we draw the axis labels. + pub rect: Rect, + pub transform: Option, + pub steps: Arc>, } impl AxisWidget { /// if `rect` as width or height == 0, is will be automatically calculated from ticks and text. - pub(super) fn new(hints: AxisHints, rect: Rect) -> Self { + pub fn new(hints: AxisHints, rect: Rect) -> Self { Self { range: (0.0..=0.0), hints, @@ -230,70 +252,76 @@ impl AxisWidget { } } - pub fn ui(self, ui: &mut Ui, axis: Axis) -> Response { + /// Returns the actual thickness of the axis. + pub fn ui(self, ui: &mut Ui, axis: Axis) -> (Response, f32) { let response = ui.allocate_rect(self.rect, Sense::hover()); if !ui.is_rect_visible(response.rect) { - return response; + return (response, 0.0); } let visuals = ui.style().visuals.clone(); - let text = self.hints.label; - let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body); - let text_color = visuals - .override_text_color - .unwrap_or_else(|| ui.visuals().text_color()); - let angle: f32 = match axis { - Axis::X => 0.0, - Axis::Y => -std::f32::consts::TAU * 0.25, - }; - // select text_pos and angle depending on placement and orientation of widget - let text_pos = match self.hints.placement { - Placement::LeftBottom => match axis { - Axis::X => { - let pos = response.rect.center_bottom(); - Pos2 { - x: pos.x - galley.size().x / 2.0, - y: pos.y - galley.size().y * 1.25, + + { + let text = self.hints.label; + let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body); + let text_color = visuals + .override_text_color + .unwrap_or_else(|| ui.visuals().text_color()); + let angle: f32 = match axis { + Axis::X => 0.0, + Axis::Y => -std::f32::consts::TAU * 0.25, + }; + // select text_pos and angle depending on placement and orientation of widget + let text_pos = match self.hints.placement { + Placement::LeftBottom => match axis { + Axis::X => { + let pos = response.rect.center_bottom(); + Pos2 { + x: pos.x - galley.size().x / 2.0, + y: pos.y - galley.size().y * 1.25, + } } - } - Axis::Y => { - let pos = response.rect.left_center(); - Pos2 { - x: pos.x, - y: pos.y + galley.size().x / 2.0, + Axis::Y => { + let pos = response.rect.left_center(); + Pos2 { + x: pos.x, + y: pos.y + galley.size().x / 2.0, + } } - } - }, - Placement::RightTop => match axis { - Axis::X => { - let pos = response.rect.center_top(); - Pos2 { - x: pos.x - galley.size().x / 2.0, - y: pos.y + galley.size().y * 0.25, + }, + Placement::RightTop => match axis { + Axis::X => { + let pos = response.rect.center_top(); + Pos2 { + x: pos.x - galley.size().x / 2.0, + y: pos.y + galley.size().y * 0.25, + } } - } - Axis::Y => { - let pos = response.rect.right_center(); - Pos2 { - x: pos.x - galley.size().y * 1.5, - y: pos.y + galley.size().x / 2.0, + Axis::Y => { + let pos = response.rect.right_center(); + Pos2 { + x: pos.x - galley.size().y * 1.5, + y: pos.y + galley.size().x / 2.0, + } } - } - }, - }; + }, + }; - ui.painter() - .add(TextShape::new(text_pos, galley, text_color).with_angle(angle)); + ui.painter() + .add(TextShape::new(text_pos, galley, text_color).with_angle(angle)); + } - // --- add ticks --- let font_id = TextStyle::Body.resolve(ui.style()); let Some(transform) = self.transform else { - return response; + return (response, 0.0); }; let label_spacing = self.hints.label_spacing; + let mut thickness: f32 = 0.0; + + // Add tick labels: for step in self.steps.iter() { let text = (self.hints.formatter)(*step, self.hints.digits, &self.range); if !text.is_empty() { @@ -314,41 +342,61 @@ impl AxisWidget { .layout_no_wrap(text, font_id.clone(), text_color); if spacing_in_points < galley.size()[axis as usize] { - continue; // the galley won't fit + continue; // the galley won't fit (likely too wide on the X axis). } - let text_pos = match axis { + match axis { Axis::X => { - let y = match self.hints.placement { - Placement::LeftBottom => self.rect.min.y, - Placement::RightTop => self.rect.max.y - galley.size().y, - }; + thickness = thickness.max(galley.size().y); + let projected_point = super::PlotPoint::new(step.value, 0.0); - Pos2 { - x: transform.position_from_point(&projected_point).x - - galley.size().x / 2.0, - y, - } + let center_x = transform.position_from_point(&projected_point).x; + let y = match VPlacement::from(self.hints.placement) { + VPlacement::Bottom => self.rect.min.y, + VPlacement::Top => self.rect.max.y - galley.size().y, + }; + let pos = Pos2::new(center_x - galley.size().x / 2.0, y); + ui.painter().add(TextShape::new(pos, galley, text_color)); } Axis::Y => { - let x = match self.hints.placement { - Placement::LeftBottom => self.rect.max.x - galley.size().x, - Placement::RightTop => self.rect.min.x, - }; + thickness = thickness.max(galley.size().x); + let projected_point = super::PlotPoint::new(0.0, step.value); - Pos2 { - x, - y: transform.position_from_point(&projected_point).y - - galley.size().y / 2.0, - } + let center_y = transform.position_from_point(&projected_point).y; + + match HPlacement::from(self.hints.placement) { + HPlacement::Left => { + let angle = 0.0; // TODO: allow users to rotate text + + if angle == 0.0 { + let x = self.rect.max.x - galley.size().x; + let pos = Pos2::new(x, center_y - galley.size().y / 2.0); + ui.painter().add(TextShape::new(pos, galley, text_color)); + } else { + let right = Pos2::new( + self.rect.max.x, + center_y - galley.size().y / 2.0, + ); + let width = galley.size().x; + let left = + right - Rot2::from_angle(angle) * Vec2::new(width, 0.0); + + ui.painter().add( + TextShape::new(left, galley, text_color).with_angle(angle), + ); + } + } + HPlacement::Right => { + let x = self.rect.min.x; + let pos = Pos2::new(x, center_y - galley.size().y / 2.0); + ui.painter().add(TextShape::new(pos, galley, text_color)); + } + }; } }; - - ui.painter() - .add(Shape::galley(text_pos, galley, text_color)); } } - response + (response, thickness) } } diff --git a/crates/egui_plot/src/items/mod.rs b/crates/egui_plot/src/items/mod.rs index a3b1ae8cbc9..fb176438e3c 100644 --- a/crates/egui_plot/src/items/mod.rs +++ b/crates/egui_plot/src/items/mod.rs @@ -49,6 +49,8 @@ pub(super) trait PlotItem { fn bounds(&self) -> PlotBounds; + fn id(&self) -> Option; + fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { match self.geometry() { PlotGeometry::None => None, @@ -120,6 +122,7 @@ pub struct HLine { pub(super) name: String, pub(super) highlight: bool, pub(super) style: LineStyle, + id: Option, } impl HLine { @@ -130,6 +133,7 @@ impl HLine { name: String::default(), highlight: false, style: LineStyle::Solid, + id: None, } } @@ -180,6 +184,13 @@ impl HLine { self.name = name.to_string(); self } + + /// Set the line's id which is used to identify it in the plot's response. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } } impl PlotItem for HLine { @@ -232,6 +243,10 @@ impl PlotItem for HLine { bounds.max[1] = self.y; bounds } + + fn id(&self) -> Option { + self.id + } } /// A vertical line in a plot, filling the full width @@ -242,6 +257,7 @@ pub struct VLine { pub(super) name: String, pub(super) highlight: bool, pub(super) style: LineStyle, + id: Option, } impl VLine { @@ -252,6 +268,7 @@ impl VLine { name: String::default(), highlight: false, style: LineStyle::Solid, + id: None, } } @@ -302,6 +319,13 @@ impl VLine { self.name = name.to_string(); self } + + /// Set the line's id which is used to identify it in the plot's response. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } } impl PlotItem for VLine { @@ -354,6 +378,10 @@ impl PlotItem for VLine { bounds.max[0] = self.x; bounds } + + fn id(&self) -> Option { + self.id + } } /// A series of values forming a path. @@ -364,6 +392,7 @@ pub struct Line { pub(super) highlight: bool, pub(super) fill: Option, pub(super) style: LineStyle, + id: Option, } impl Line { @@ -375,6 +404,7 @@ impl Line { highlight: false, fill: None, style: LineStyle::Solid, + id: None, } } @@ -432,6 +462,13 @@ impl Line { self.name = name.to_string(); self } + + /// Set the line's id which is used to identify it in the plot's response. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } } /// Returns the x-coordinate of a possible intersection between a line segment from `p1` to `p2` and @@ -528,6 +565,10 @@ impl PlotItem for Line { fn bounds(&self) -> PlotBounds { self.series.bounds() } + + fn id(&self) -> Option { + self.id + } } /// A convex polygon. @@ -538,6 +579,7 @@ pub struct Polygon { pub(super) highlight: bool, pub(super) fill_color: Option, pub(super) style: LineStyle, + id: Option, } impl Polygon { @@ -549,6 +591,7 @@ impl Polygon { highlight: false, fill_color: None, style: LineStyle::Solid, + id: None, } } @@ -600,6 +643,13 @@ impl Polygon { self.name = name.to_string(); self } + + /// Set the polygon's id which is used to identify it in the plot's response. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } } impl PlotItem for Polygon { @@ -654,6 +704,10 @@ impl PlotItem for Polygon { fn bounds(&self) -> PlotBounds { self.series.bounds() } + + fn id(&self) -> Option { + self.id + } } /// Text inside the plot. @@ -665,6 +719,7 @@ pub struct Text { pub(super) highlight: bool, pub(super) color: Color32, pub(super) anchor: Align2, + id: Option, } impl Text { @@ -676,6 +731,7 @@ impl Text { highlight: false, color: Color32::TRANSPARENT, anchor: Align2::CENTER_CENTER, + id: None, } } @@ -712,6 +768,13 @@ impl Text { self.name = name.to_string(); self } + + /// Set the text's id which is used to identify it in the plot's response. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } } impl PlotItem for Text { @@ -768,6 +831,10 @@ impl PlotItem for Text { bounds.extend_with(&self.position); bounds } + + fn id(&self) -> Option { + self.id + } } /// A set of points. @@ -790,6 +857,7 @@ pub struct Points { pub(super) highlight: bool, pub(super) stems: Option, + id: Option, } impl Points { @@ -803,6 +871,7 @@ impl Points { name: Default::default(), highlight: false, stems: None, + id: None, } } @@ -860,6 +929,13 @@ impl Points { self.name = name.to_string(); self } + + /// Set the points' id which is used to identify them in the plot's response. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } } impl PlotItem for Points { @@ -1018,6 +1094,10 @@ impl PlotItem for Points { fn bounds(&self) -> PlotBounds { self.series.bounds() } + + fn id(&self) -> Option { + self.id + } } /// A set of arrows. @@ -1028,6 +1108,7 @@ pub struct Arrows { pub(super) color: Color32, pub(super) name: String, pub(super) highlight: bool, + id: Option, } impl Arrows { @@ -1039,6 +1120,7 @@ impl Arrows { color: Color32::TRANSPARENT, name: Default::default(), highlight: false, + id: None, } } @@ -1075,6 +1157,13 @@ impl Arrows { self.name = name.to_string(); self } + + /// Set the arrows' id which is used to identify them in the plot's response. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } } impl PlotItem for Arrows { @@ -1150,6 +1239,10 @@ impl PlotItem for Arrows { fn bounds(&self) -> PlotBounds { self.origins.bounds() } + + fn id(&self) -> Option { + self.id + } } /// An image in the plot. @@ -1164,6 +1257,7 @@ pub struct PlotImage { pub(super) tint: Color32, pub(super) highlight: bool, pub(super) name: String, + id: Option, } impl PlotImage { @@ -1183,6 +1277,7 @@ impl PlotImage { rotation: 0.0, bg_fill: Default::default(), tint: Color32::WHITE, + id: None, } } @@ -1330,6 +1425,10 @@ impl PlotItem for PlotImage { bounds.extend_with(&right_bottom); bounds } + + fn id(&self) -> Option { + self.id + } } // ---------------------------------------------------------------------------- @@ -1344,6 +1443,7 @@ pub struct BarChart { pub(super) element_formatter: Option String>>, highlight: bool, + id: Option, } impl BarChart { @@ -1355,6 +1455,7 @@ impl BarChart { name: String::new(), element_formatter: None, highlight: false, + id: None, } } @@ -1454,6 +1555,13 @@ impl BarChart { } self } + + /// Set the bar chart's id which is used to identify it in the plot's response. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } } impl PlotItem for BarChart { @@ -1512,6 +1620,10 @@ impl PlotItem for BarChart { bar.add_shapes(plot.transform, true, shapes); bar.add_rulers_and_text(self, plot, shapes, cursors); } + + fn id(&self) -> Option { + self.id + } } /// A diagram containing a series of [`BoxElem`] elements. @@ -1524,6 +1636,7 @@ pub struct BoxPlot { pub(super) element_formatter: Option String>>, highlight: bool, + id: Option, } impl BoxPlot { @@ -1535,6 +1648,7 @@ impl BoxPlot { name: String::new(), element_formatter: None, highlight: false, + id: None, } } @@ -1602,6 +1716,13 @@ impl BoxPlot { self.element_formatter = Some(formatter); self } + + /// Set the box plot's id which is used to identify it in the plot's response. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } } impl PlotItem for BoxPlot { @@ -1660,6 +1781,10 @@ impl PlotItem for BoxPlot { box_plot.add_shapes(plot.transform, true, shapes); box_plot.add_rulers_and_text(self, plot, shapes, cursors); } + + fn id(&self) -> Option { + self.id + } } // ---------------------------------------------------------------------------- diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 4fe89ff367d..dc1be39f960 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -117,6 +117,11 @@ pub struct PlotResponse { /// The transform between screen coordinates and plot coordinates. pub transform: PlotTransform, + + /// The id of a currently hovered item if any. + /// + /// This is `None` if either no item was hovered, or the hovered item didn't provide an id. + pub hovered_plot_item: Option, } // ---------------------------------------------------------------------------- @@ -768,84 +773,29 @@ impl Plot { .at_least(min_size.y); vec2(width, height) }; + // Determine complete rect of widget. let complete_rect = Rect { min: pos, max: pos + size, }; - // Next we want to create this layout. - // Indices are only examples. - // - // left right - // +---+---------x----------+ + - // | | X-axis 3 | - // | +--------------------+ top - // | | X-axis 2 | - // +-+-+--------------------+-+-+ - // |y|y| |y|y| - // |-|-| |-|-| - // |A|A| |A|A| - // y|x|x| Plot Window |x|x| - // |i|i| |i|i| - // |s|s| |s|s| - // |1|0| |2|3| - // +-+-+--------------------+-+-+ - // | X-axis 0 | | - // +--------------------+ | bottom - // | X-axis 1 | | - // + +--------------------+---+ - // - - let mut plot_rect: Rect = { - // Calcuclate the space needed for each axis labels. - let mut margin = Margin::ZERO; - if show_axes.x { - for cfg in &x_axes { - match cfg.placement { - axis::Placement::LeftBottom => { - margin.bottom += cfg.thickness(Axis::X); - } - axis::Placement::RightTop => { - margin.top += cfg.thickness(Axis::X); - } - } - } - } - if show_axes.y { - for cfg in &y_axes { - match cfg.placement { - axis::Placement::LeftBottom => { - margin.left += cfg.thickness(Axis::Y); - } - axis::Placement::RightTop => { - margin.right += cfg.thickness(Axis::Y); - } - } - } - } - // determine plot rectangle - margin.shrink_rect(complete_rect) - }; - - let [mut x_axis_widgets, mut y_axis_widgets] = - axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]); + let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); - // If too little space, remove axis widgets - if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 { - y_axis_widgets.clear(); - x_axis_widgets.clear(); - plot_rect = complete_rect; - } + let ([x_axis_widgets, y_axis_widgets], plot_rect) = axis_widgets( + PlotMemory::load(ui.ctx(), plot_id).as_ref(), // TODO: avoid loading plot memory twice + show_axes, + complete_rect, + [&x_axes, &y_axes], + ); // Allocate the plot window. let response = ui.allocate_rect(plot_rect, Sense::click_and_drag()); - let rect = plot_rect; // Load or initialize the memory. - let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); - ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); - let memory = if reset { + ui.ctx().check_for_id_clash(plot_id, plot_rect, "Plot"); + + let mut mem = if reset { if let Some((name, _)) = linked_axes.as_ref() { ui.data_mut(|data| { let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); @@ -858,26 +808,22 @@ impl Plot { } .unwrap_or_else(|| PlotMemory { auto_bounds: default_auto_bounds, - hovered_item: None, + hovered_legend_item: None, hidden_items: Default::default(), - transform: PlotTransform::new(rect, min_auto_bounds, center_axis.x, center_axis.y), + transform: PlotTransform::new(plot_rect, min_auto_bounds, center_axis.x, center_axis.y), last_click_pos_for_zoom: None, + x_axis_thickness: Default::default(), + y_axis_thickness: Default::default(), }); - let PlotMemory { - mut auto_bounds, - mut hovered_item, - mut hidden_items, - transform: last_plot_transform, - mut last_click_pos_for_zoom, - } = memory; + let last_plot_transform = mem.transform; // Call the plot build function. let mut plot_ui = PlotUi { items: Vec::new(), next_auto_color_idx: 0, last_plot_transform, - last_auto_bounds: auto_bounds, + last_auto_bounds: mem.auto_bounds, response, bounds_modifications: Vec::new(), ctx: ui.ctx().clone(), @@ -894,9 +840,9 @@ impl Plot { // Background if show_background { ui.painter() - .with_clip_rect(rect) + .with_clip_rect(plot_rect) .add(epaint::RectShape::new( - rect, + plot_rect, Rounding::same(2.0), ui.visuals().extreme_bg_color, ui.visuals().widgets.noninteractive.bg_stroke, @@ -905,16 +851,16 @@ impl Plot { // --- Legend --- let legend = legend_config - .and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items)); + .and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items)); // Don't show hover cursor when hovering over legend. - if hovered_item.is_some() { + if mem.hovered_legend_item.is_some() { show_x = false; show_y = false; } // Remove the deselected items. - items.retain(|item| !hidden_items.contains(item.name())); + items.retain(|item| !mem.hidden_items.contains(item.name())); // Highlight the hovered items. - if let Some(hovered_name) = &hovered_item { + if let Some(hovered_name) = &mem.hovered_legend_item { items .iter_mut() .filter(|entry| entry.name() == hovered_name) @@ -961,11 +907,11 @@ impl Plot { if let Some(linked_bounds) = link_groups.0.get(id) { if axes.x { bounds.set_x(&linked_bounds.bounds); - auto_bounds.x = linked_bounds.auto_bounds.x; + mem.auto_bounds.x = linked_bounds.auto_bounds.x; } if axes.y { bounds.set_y(&linked_bounds.bounds); - auto_bounds.y = linked_bounds.auto_bounds.y; + mem.auto_bounds.y = linked_bounds.auto_bounds.y; } }; }); @@ -973,7 +919,7 @@ impl Plot { // Allow double-clicking to reset to the initial bounds. if allow_double_click_reset && response.double_clicked() { - auto_bounds = true.into(); + mem.auto_bounds = true.into(); } // Apply bounds modifications. @@ -981,30 +927,32 @@ impl Plot { match modification { BoundsModification::Set(new_bounds) => { bounds = new_bounds; - auto_bounds = false.into(); + mem.auto_bounds = false.into(); } BoundsModification::Translate(delta) => { bounds.translate(delta); - auto_bounds = false.into(); + mem.auto_bounds = false.into(); + } + BoundsModification::AutoBounds(new_auto_bounds) => { + mem.auto_bounds = new_auto_bounds; } - BoundsModification::AutoBounds(new_auto_bounds) => auto_bounds = new_auto_bounds, BoundsModification::Zoom(zoom_factor, center) => { bounds.zoom(zoom_factor, center); - auto_bounds = false.into(); + mem.auto_bounds = false.into(); } } } // Reset bounds to initial bounds if they haven't been modified. - if auto_bounds.x { + if mem.auto_bounds.x { bounds.set_x(&min_auto_bounds); } - if auto_bounds.y { + if mem.auto_bounds.y { bounds.set_y(&min_auto_bounds); } - let auto_x = auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x); - let auto_y = auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y); + let auto_x = mem.auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x); + let auto_y = mem.auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y); // Set bounds automatically based on content. if auto_x || auto_y { @@ -1027,17 +975,19 @@ impl Plot { } } - let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.y); + mem.transform = PlotTransform::new(plot_rect, bounds, center_axis.x, center_axis.y); // Enforce aspect ratio if let Some(data_aspect) = data_aspect { if let Some((_, linked_axes)) = &linked_axes { let change_x = linked_axes.y && !linked_axes.x; - transform.set_aspect_by_changing_axis(data_aspect as f64, change_x); + mem.transform + .set_aspect_by_changing_axis(data_aspect as f64, change_x); } else if default_auto_bounds.any() { - transform.set_aspect_by_expanding(data_aspect as f64); + mem.transform.set_aspect_by_expanding(data_aspect as f64); } else { - transform.set_aspect_by_changing_axis(data_aspect as f64, false); + mem.transform + .set_aspect_by_changing_axis(data_aspect as f64, false); } } @@ -1051,8 +1001,8 @@ impl Plot { if !allow_drag.y { delta.y = 0.0; } - transform.translate_bounds(delta); - auto_bounds = !allow_drag; + mem.transform.translate_bounds(delta); + mem.auto_bounds = !allow_drag; } // Zooming @@ -1061,9 +1011,9 @@ impl Plot { // Save last click to allow boxed zooming if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) { // it would be best for egui that input has a memory of the last click pos because it's a common pattern - last_click_pos_for_zoom = response.hover_pos(); + mem.last_click_pos_for_zoom = response.hover_pos(); } - let box_start_pos = last_click_pos_for_zoom; + let box_start_pos = mem.last_click_pos_for_zoom; let box_end_pos = response.hover_pos(); if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) { // while dragging prepare a Shape and draw it later on top of the plot @@ -1085,8 +1035,8 @@ impl Plot { } // when the click is release perform the zoom if response.drag_released() { - let box_start_pos = transform.value_from_position(box_start_pos); - let box_end_pos = transform.value_from_position(box_end_pos); + let box_start_pos = mem.transform.value_from_position(box_start_pos); + let box_end_pos = mem.transform.value_from_position(box_end_pos); let new_bounds = PlotBounds { min: [ box_start_pos.x.min(box_end_pos.x), @@ -1098,11 +1048,11 @@ impl Plot { ], }; if new_bounds.is_valid() { - transform.set_bounds(new_bounds); - auto_bounds = false.into(); + mem.transform.set_bounds(new_bounds); + mem.auto_bounds = false.into(); } // reset the boxed zoom state - last_click_pos_for_zoom = None; + mem.last_click_pos_for_zoom = None; } } } @@ -1122,15 +1072,15 @@ impl Plot { zoom_factor.y = 1.0; } if zoom_factor != Vec2::splat(1.0) { - transform.zoom(zoom_factor, hover_pos); - auto_bounds = !allow_zoom; + mem.transform.zoom(zoom_factor, hover_pos); + mem.auto_bounds = !allow_zoom; } } if allow_scroll { let scroll_delta = ui.input(|i| i.smooth_scroll_delta); if scroll_delta != Vec2::ZERO { - transform.translate_bounds(-scroll_delta); - auto_bounds = false.into(); + mem.transform.translate_bounds(-scroll_delta); + mem.auto_bounds = false.into(); } } } @@ -1138,12 +1088,12 @@ impl Plot { // --- transform initialized // Add legend widgets to plot - let bounds = transform.bounds(); + let bounds = mem.transform.bounds(); let x_axis_range = bounds.range_x(); let x_steps = Arc::new({ let input = GridInput { bounds: (bounds.min[0], bounds.max[0]), - base_step_size: transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64, + base_step_size: mem.transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64, }; (grid_spacers[0])(input) }); @@ -1151,26 +1101,28 @@ impl Plot { let y_steps = Arc::new({ let input = GridInput { bounds: (bounds.min[1], bounds.max[1]), - base_step_size: transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64, + base_step_size: mem.transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64, }; (grid_spacers[1])(input) }); - for mut widget in x_axis_widgets { + for (i, mut widget) in x_axis_widgets.into_iter().enumerate() { widget.range = x_axis_range.clone(); - widget.transform = Some(transform); + widget.transform = Some(mem.transform); widget.steps = x_steps.clone(); - widget.ui(ui, Axis::X); + let (_response, thickness) = widget.ui(ui, Axis::X); + mem.x_axis_thickness.insert(i, thickness); } - for mut widget in y_axis_widgets { + for (i, mut widget) in y_axis_widgets.into_iter().enumerate() { widget.range = y_axis_range.clone(); - widget.transform = Some(transform); + widget.transform = Some(mem.transform); widget.steps = y_steps.clone(); - widget.ui(ui, Axis::Y); + let (_response, thickness) = widget.ui(ui, Axis::Y); + mem.y_axis_thickness.insert(i, thickness); } // Initialize values from functions. for item in &mut items { - item.initialize(transform.bounds().range_x()); + item.initialize(mem.transform.bounds().range_x()); } let prepared = PreparedPlot { @@ -1181,7 +1133,7 @@ impl Plot { coordinates_formatter, show_grid, grid_spacing, - transform, + transform: mem.transform, draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x), draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y), draw_cursors, @@ -1190,17 +1142,21 @@ impl Plot { clamp_grid, }; - let plot_cursors = prepared.ui(ui, &response); + let (plot_cursors, hovered_plot_item) = prepared.ui(ui, &response); if let Some(boxed_zoom_rect) = boxed_zoom_rect { - ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.0); - ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.1); + ui.painter() + .with_clip_rect(plot_rect) + .add(boxed_zoom_rect.0); + ui.painter() + .with_clip_rect(plot_rect) + .add(boxed_zoom_rect.1); } if let Some(mut legend) = legend { ui.add(&mut legend); - hidden_items = legend.hidden_items(); - hovered_item = legend.hovered_item_name(); + mem.hidden_items = legend.hidden_items(); + mem.hovered_legend_item = legend.hovered_item_name(); } if let Some((id, _)) = linked_cursors.as_ref() { @@ -1222,107 +1178,142 @@ impl Plot { link_groups.0.insert( *id, LinkedBounds { - bounds: *transform.bounds(), - auto_bounds, + bounds: *mem.transform.bounds(), + auto_bounds: mem.auto_bounds, }, ); }); } - let memory = PlotMemory { - auto_bounds, - hovered_item, - hidden_items, - transform, - last_click_pos_for_zoom, - }; - memory.store(ui.ctx(), plot_id); + let transform = mem.transform; + mem.store(ui.ctx(), plot_id); let response = if show_x || show_y { response.on_hover_cursor(CursorIcon::Crosshair) } else { response }; + ui.advance_cursor_after_rect(complete_rect); + PlotResponse { inner, response, transform, + hovered_plot_item, } } } +/// Returns the rect left after adding axes. fn axis_widgets( + mem: Option<&PlotMemory>, show_axes: Vec2b, - plot_rect: Rect, + complete_rect: Rect, [x_axes, y_axes]: [&[AxisHints]; 2], -) -> [Vec; 2] { +) -> ([Vec; 2], Rect) { + // Next we want to create this layout. + // Indices are only examples. + // + // left right + // +---+---------x----------+ + + // | | X-axis 3 | + // | +--------------------+ top + // | | X-axis 2 | + // +-+-+--------------------+-+-+ + // |y|y| |y|y| + // |-|-| |-|-| + // |A|A| |A|A| + // y|x|x| Plot Window |x|x| + // |i|i| |i|i| + // |s|s| |s|s| + // |1|0| |2|3| + // +-+-+--------------------+-+-+ + // | X-axis 0 | | + // +--------------------+ | bottom + // | X-axis 1 | | + // + +--------------------+---+ + // + let mut x_axis_widgets = Vec::::new(); let mut y_axis_widgets = Vec::::new(); - // Widget count per border of plot in order left, top, right, bottom - struct NumWidgets { - left: usize, - top: usize, - right: usize, - bottom: usize, - } - let mut num_widgets = NumWidgets { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; + // Will shrink as we add more axes. + let mut rect_left = complete_rect; + if show_axes.x { - for cfg in x_axes { - let size_y = Vec2::new(0.0, cfg.thickness(Axis::X)); - let rect = match cfg.placement { - axis::Placement::LeftBottom => { - let off = num_widgets.bottom as f32; - num_widgets.bottom += 1; - Rect { - min: plot_rect.left_bottom() + size_y * off, - max: plot_rect.right_bottom() + size_y * (off + 1.0), - } + // We will fix this later, once we know how much space the y axes take up. + let initial_x_range = complete_rect.x_range(); + + for (i, cfg) in x_axes.iter().enumerate().rev() { + let mut height = cfg.thickness(Axis::X); + if let Some(mem) = mem { + // If the labels took up too much space the previous frame, give them more space now: + height = height.max(mem.x_axis_thickness.get(&i).copied().unwrap_or_default()); + } + + let rect = match VPlacement::from(cfg.placement) { + VPlacement::Bottom => { + let bottom = rect_left.bottom(); + *rect_left.bottom_mut() -= height; + let top = rect_left.bottom(); + Rect::from_x_y_ranges(initial_x_range, top..=bottom) } - axis::Placement::RightTop => { - let off = num_widgets.top as f32; - num_widgets.top += 1; - Rect { - min: plot_rect.left_top() - size_y * (off + 1.0), - max: plot_rect.right_top() - size_y * off, - } + VPlacement::Top => { + let top = rect_left.top(); + *rect_left.top_mut() += height; + let bottom = rect_left.top(); + Rect::from_x_y_ranges(initial_x_range, top..=bottom) } }; x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); } } if show_axes.y { - for cfg in y_axes { - let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0); - let rect = match cfg.placement { - axis::Placement::LeftBottom => { - let off = num_widgets.left as f32; - num_widgets.left += 1; - Rect { - min: plot_rect.left_top() - size_x * (off + 1.0), - max: plot_rect.left_bottom() - size_x * off, - } + // We know this, since we've already allocated space for the x axes. + let plot_y_range = rect_left.y_range(); + + for (i, cfg) in y_axes.iter().enumerate().rev() { + let mut width = cfg.thickness(Axis::Y); + if let Some(mem) = mem { + // If the labels took up too much space the previous frame, give them more space now: + width = width.max(mem.y_axis_thickness.get(&i).copied().unwrap_or_default()); + } + + let rect = match HPlacement::from(cfg.placement) { + HPlacement::Left => { + let left = rect_left.left(); + *rect_left.left_mut() += width; + let right = rect_left.left(); + Rect::from_x_y_ranges(left..=right, plot_y_range) } - axis::Placement::RightTop => { - let off = num_widgets.right as f32; - num_widgets.right += 1; - Rect { - min: plot_rect.right_top() + size_x * off, - max: plot_rect.right_bottom() + size_x * (off + 1.0), - } + HPlacement::Right => { + let right = rect_left.right(); + *rect_left.right_mut() -= width; + let left = rect_left.right(); + Rect::from_x_y_ranges(left..=right, plot_y_range) } }; y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); } } - [x_axis_widgets, y_axis_widgets] + let mut plot_rect = rect_left; + + // If too little space, remove axis widgets + if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 { + y_axis_widgets.clear(); + x_axis_widgets.clear(); + plot_rect = complete_rect; + } + + // Bow that we know the final x_range of the plot_rect, + // assign it to the x_axis_widgets (they are currently too wide): + for widget in &mut x_axis_widgets { + widget.rect = Rect::from_x_y_ranges(plot_rect.x_range(), widget.rect.y_range()); + } + + ([x_axis_widgets, y_axis_widgets], plot_rect) } /// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply @@ -1660,7 +1651,7 @@ struct PreparedPlot { } impl PreparedPlot { - fn ui(self, ui: &mut Ui, response: &Response) -> Vec { + fn ui(self, ui: &mut Ui, response: &Response) -> (Vec, Option) { let mut axes_shapes = Vec::new(); if self.show_grid.x { @@ -1684,10 +1675,10 @@ impl PreparedPlot { } let hover_pos = response.hover_pos(); - let cursors = if let Some(pointer) = hover_pos { + let (cursors, hovered_item_id) = if let Some(pointer) = hover_pos { self.hover(ui, pointer, &mut shapes) } else { - Vec::new() + (Vec::new(), None) }; // Draw cursors @@ -1741,7 +1732,7 @@ impl PreparedPlot { } } - cursors + (cursors, hovered_item_id) } fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) { @@ -1841,7 +1832,7 @@ impl PreparedPlot { } } - fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) -> Vec { + fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) -> (Vec, Option) { let Self { transform, show_x, @@ -1852,7 +1843,7 @@ impl PreparedPlot { } = self; if !show_x && !show_y { - return Vec::new(); + return (Vec::new(), None); } let interact_radius_sq = (16.0_f32).powi(2); @@ -1868,8 +1859,6 @@ impl PreparedPlot { .min_by_key(|(_, elem)| elem.dist_sq.ord()) .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq); - let mut cursors = Vec::new(); - let plot = items::PlotConfig { ui, transform, @@ -1877,8 +1866,11 @@ impl PreparedPlot { show_y: *show_y, }; - if let Some((item, elem)) = closest { + let mut cursors = Vec::new(); + + let hovered_plot_item_id = if let Some((item, elem)) = closest { item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter); + item.id() } else { let value = transform.value_from_position(pointer); items::rulers_at_value( @@ -1890,9 +1882,10 @@ impl PreparedPlot { &mut cursors, label_formatter, ); - } + None + }; - cursors + (cursors, hovered_plot_item_id) } } diff --git a/crates/egui_plot/src/memory.rs b/crates/egui_plot/src/memory.rs index c334f115274..6a982269f26 100644 --- a/crates/egui_plot/src/memory.rs +++ b/crates/egui_plot/src/memory.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use egui::{ahash, Context, Id, Pos2, Vec2b}; use crate::{PlotBounds, PlotTransform}; @@ -12,8 +14,8 @@ pub struct PlotMemory { /// the bounds, for example by moving or zooming. pub auto_bounds: Vec2b, - /// Which item is hovered? - pub hovered_item: Option, + /// Display string of the hovered legend item if any. + pub hovered_legend_item: Option, /// Which items _not_ to show? pub hidden_items: ahash::HashSet, @@ -23,6 +25,13 @@ pub struct PlotMemory { /// Allows to remember the first click position when performing a boxed zoom pub(crate) last_click_pos_for_zoom: Option, + + /// The thickness of each of the axes the previous frame. + /// + /// This is used in the next frame to make the axes thicker + /// in order to fit the labels, if necessary. + pub(crate) x_axis_thickness: BTreeMap, + pub(crate) y_axis_thickness: BTreeMap, } impl PlotMemory { diff --git a/crates/egui_plot/src/transform.rs b/crates/egui_plot/src/transform.rs index 722df5fcc7d..42edef0b627 100644 --- a/crates/egui_plot/src/transform.rs +++ b/crates/egui_plot/src/transform.rs @@ -359,22 +359,22 @@ impl PlotTransform { rect } - /// delta position / delta value + /// delta position / delta value = how many ui points per step in the X axis in "plot space" pub fn dpos_dvalue_x(&self) -> f64 { self.frame.width() as f64 / self.bounds.width() } - /// delta position / delta value + /// delta position / delta value = how many ui points per step in the Y axis in "plot space" pub fn dpos_dvalue_y(&self) -> f64 { -self.frame.height() as f64 / self.bounds.height() // negated y axis! } - /// delta position / delta value + /// delta position / delta value = how many ui points per step in "plot space" pub fn dpos_dvalue(&self) -> [f64; 2] { [self.dpos_dvalue_x(), self.dpos_dvalue_y()] } - /// delta value / delta position + /// delta value / delta position = how much ground do we cover in "plot space" per ui point? pub fn dvalue_dpos(&self) -> [f64; 2] { [1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()] } diff --git a/crates/emath/src/rot2.rs b/crates/emath/src/rot2.rs index 61ff3654183..da67acb0c9c 100644 --- a/crates/emath/src/rot2.rs +++ b/crates/emath/src/rot2.rs @@ -28,6 +28,7 @@ pub struct Rot2 { /// Identity rotation impl Default for Rot2 { /// Identity rotation + #[inline] fn default() -> Self { Self { s: 0.0, c: 1.0 } } @@ -39,29 +40,35 @@ impl Rot2 { /// Angle is clockwise in radians. /// A 𝞃/4 = 90° rotation means rotating the X axis to the Y axis. + #[inline] pub fn from_angle(angle: f32) -> Self { let (s, c) = angle.sin_cos(); Self { s, c } } + #[inline] pub fn angle(self) -> f32 { self.s.atan2(self.c) } /// The factor by which vectors will be scaled. + #[inline] pub fn length(self) -> f32 { self.c.hypot(self.s) } + #[inline] pub fn length_squared(self) -> f32 { self.c.powi(2) + self.s.powi(2) } + #[inline] pub fn is_finite(self) -> bool { self.c.is_finite() && self.s.is_finite() } #[must_use] + #[inline] pub fn inverse(self) -> Self { Self { s: -self.s, @@ -70,6 +77,7 @@ impl Rot2 { } #[must_use] + #[inline] pub fn normalized(self) -> Self { let l = self.length(); let ret = Self { @@ -95,6 +103,7 @@ impl std::fmt::Debug for Rot2 { impl std::ops::Mul for Rot2 { type Output = Self; + #[inline] fn mul(self, r: Self) -> Self { /* |lc -ls| * |rc -rs| @@ -111,6 +120,7 @@ impl std::ops::Mul for Rot2 { impl std::ops::Mul for Rot2 { type Output = Vec2; + #[inline] fn mul(self, v: Vec2) -> Vec2 { Vec2 { x: self.c * v.x - self.s * v.y, @@ -123,6 +133,7 @@ impl std::ops::Mul for Rot2 { impl std::ops::Mul for f32 { type Output = Rot2; + #[inline] fn mul(self, r: Rot2) -> Rot2 { Rot2 { c: self * r.c, @@ -135,6 +146,7 @@ impl std::ops::Mul for f32 { impl std::ops::Mul for Rot2 { type Output = Self; + #[inline] fn mul(self, r: f32) -> Self { Self { c: self.c * r, @@ -147,6 +159,7 @@ impl std::ops::Mul for Rot2 { impl std::ops::Div for Rot2 { type Output = Self; + #[inline] fn div(self, r: f32) -> Self { Self { c: self.c / r, diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index df9ee4077d8..12824f9fb7b 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -63,6 +63,16 @@ log = ["dep:log"] ## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra). mint = ["emath/mint"] +## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. +## +## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. +puffin = ["dep:puffin"] + +## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). +## +## This can help performance for graphics-intense applications. +rayon = ["dep:rayon"] + ## Allow serialization using [`serde`](https://docs.rs/serde). serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde"] @@ -88,6 +98,8 @@ bytemuck = { version = "1.7.2", optional = true, features = ["derive"] } document-features = { version = "0.2", optional = true } log = { version = "0.4", optional = true, features = ["std"] } +puffin = { workspace = true, optional = true } +rayon = { version = "1.7", optional = true } ## Allow serialization using [`serde`](https://docs.rs/serde) . serde = { version = "1", optional = true, features = ["derive", "rc"] } diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 7f7c9f1b1c7..709adbfae9c 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -60,14 +60,14 @@ fn tessellate_circles(c: &mut Criterion) { let prepared_discs = atlas.prepared_discs(); b.iter(|| { - let clipped_primitive = tessellate_shapes( + let mut tessellator = Tessellator::new( pixels_per_point, options, font_tex_size, prepared_discs.clone(), - clipped_shapes.clone(), ); - black_box(clipped_primitive); + let clipped_primitives = tessellator.tessellate_shapes(clipped_shapes.clone()); + black_box(clipped_primitives); }); }); } diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 18193dba132..c83be860a2a 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -52,13 +52,16 @@ pub use { }, stats::PaintStats, stroke::Stroke, - tessellator::{tessellate_shapes, TessellationOptions, Tessellator}, + tessellator::{TessellationOptions, Tessellator}, text::{FontFamily, FontId, Fonts, Galley}, texture_atlas::TextureAtlas, texture_handle::TextureHandle, textures::TextureManager, }; +#[allow(deprecated)] +pub use tessellator::tessellate_shapes; + pub use ecolor::{Color32, Hsva, HsvaGamma, Rgba}; pub use emath::{pos2, vec2, Pos2, Rect, Vec2}; @@ -172,3 +175,38 @@ pub(crate) fn f64_hash(state: &mut H, f: f64) { f.to_bits().hash(state); } } + +// --------------------------------------------------------------------------- + +/// Was epaint compiled with the `rayon` feature? +pub const HAS_RAYON: bool = cfg!(feature = "rayon"); + +// --------------------------------------------------------------------------- + +mod profiling_scopes { + #![allow(unused_macros)] + #![allow(unused_imports)] + + /// Profiling macro for feature "puffin" + macro_rules! profile_function { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_function!($($arg)*); + }; + } + pub(crate) use profile_function; + + /// Profiling macro for feature "puffin" + macro_rules! profile_scope { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_scope!($($arg)*); + }; + } + pub(crate) use profile_scope; +} + +#[allow(unused_imports)] +pub(crate) use profiling_scopes::*; diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 787c10d8200..7d5a51965d9 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -84,6 +84,8 @@ impl Mesh { /// Are all indices within the bounds of the contained vertices? pub fn is_valid(&self) -> bool { + crate::profile_function!(); + if let Ok(n) = u32::try_from(self.vertices.len()) { self.indices.iter().all(|&i| i < n) } else { @@ -106,6 +108,7 @@ impl Mesh { /// Append all the indices and vertices of `other` to `self`. pub fn append(&mut self, other: Self) { + crate::profile_function!(); crate::epaint_assert!(other.is_valid()); if self.is_empty() { diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 26e655f32d7..1ccc4bce81c 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -756,6 +756,10 @@ pub struct TextShape { /// This only affects the glyphs and will NOT replace background color nor strikethrough/underline color. pub override_text_color: Option, + /// If set, the text will be rendered with the given opacity in gamma space + /// Affects everything: backgrounds, glyphs, strikethough, underline, etc. + pub opacity_factor: f32, + /// Rotate text by this many radians clockwise. /// The pivot is `pos` (the upper left corner of the text). pub angle: f32, @@ -773,6 +777,7 @@ impl TextShape { underline: Stroke::NONE, fallback_color, override_text_color: None, + opacity_factor: 1.0, angle: 0.0, } } @@ -803,6 +808,13 @@ impl TextShape { self.angle = angle; self } + + /// Render text with this opacity in gamma space + #[inline] + pub fn with_opacity_factor(mut self, opacity_factor: f32) -> Self { + self.opacity_factor = opacity_factor; + self + } } impl From for Shape { diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index b36accb5a3f..c8edff1fcaf 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -56,6 +56,7 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { underline, fallback_color, override_text_color, + opacity_factor: _, angle: _, }) => { adjust_color(&mut underline.color); diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 72899f2844c..925524f2f93 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -654,6 +654,15 @@ pub struct TessellationOptions { /// The default value will be 1.0e-5, it will be used during float compare. pub epsilon: f32, + + /// If `rayon` feature is activated, should we parallelize tessellation? + pub parallel_tessellation: bool, + + /// If `true`, invalid meshes will be silently ignored. + /// If `false`, invalid meshes will cause a panic. + /// + /// The default is `false` to save performance. + pub validate_meshes: bool, } impl Default for TessellationOptions { @@ -669,6 +678,8 @@ impl Default for TessellationOptions { debug_ignore_clip_rects: false, bezier_tolerance: 0.1, epsilon: 1.0e-5, + parallel_tessellation: true, + validate_meshes: false, } } } @@ -1065,6 +1076,7 @@ fn mul_color(color: Color32, factor: f32) -> Color32 { /// For performance reasons it is smart to reuse the same [`Tessellator`]. /// /// See also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`]. +#[derive(Clone)] pub struct Tessellator { pixels_per_point: f32, options: TessellationOptions, @@ -1086,6 +1098,9 @@ pub struct Tessellator { impl Tessellator { /// Create a new [`Tessellator`]. /// + /// * `pixels_per_point`: number of physical pixels to each logical point + /// * `options`: tessellation quality + /// * `shapes`: what to tessellate /// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text. /// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec. pub fn new( @@ -1132,31 +1147,22 @@ impl Tessellator { clipped_shape: ClippedShape, out_primitives: &mut Vec, ) { - let ClippedShape { - clip_rect: new_clip_rect, - shape: new_shape, - } = clipped_shape; + let ClippedShape { clip_rect, shape } = clipped_shape; - if !new_clip_rect.is_positive() { + if !clip_rect.is_positive() { return; // skip empty clip rectangles } - if let Shape::Vec(shapes) = new_shape { + if let Shape::Vec(shapes) = shape { for shape in shapes { - self.tessellate_clipped_shape( - ClippedShape { - clip_rect: new_clip_rect, - shape, - }, - out_primitives, - ); + self.tessellate_clipped_shape(ClippedShape { clip_rect, shape }, out_primitives); } return; } - if let Shape::Callback(callback) = new_shape { + if let Shape::Callback(callback) = shape { out_primitives.push(ClippedPrimitive { - clip_rect: new_clip_rect, + clip_rect, primitive: Primitive::Callback(callback), }); return; @@ -1165,10 +1171,10 @@ impl Tessellator { let start_new_mesh = match out_primitives.last() { None => true, Some(output_clipped_primitive) => { - output_clipped_primitive.clip_rect != new_clip_rect + output_clipped_primitive.clip_rect != clip_rect || match &output_clipped_primitive.primitive { Primitive::Mesh(output_mesh) => { - output_mesh.texture_id != new_shape.texture_id() + output_mesh.texture_id != shape.texture_id() } Primitive::Callback(_) => true, } @@ -1177,7 +1183,7 @@ impl Tessellator { if start_new_mesh { out_primitives.push(ClippedPrimitive { - clip_rect: new_clip_rect, + clip_rect, primitive: Primitive::Mesh(Mesh::default()), }); } @@ -1185,8 +1191,8 @@ impl Tessellator { let out = out_primitives.last_mut().unwrap(); if let Primitive::Mesh(out_mesh) = &mut out.primitive { - self.clip_rect = new_clip_rect; - self.tessellate_shape(new_shape, out_mesh); + self.clip_rect = clip_rect; + self.tessellate_shape(shape, out_mesh); } else { unreachable!(); } @@ -1199,6 +1205,8 @@ impl Tessellator { /// * `shape`: the shape to tessellate. /// * `out`: triangles are appended to this. pub fn tessellate_shape(&mut self, shape: Shape, out: &mut Mesh) { + crate::profile_function!(); + match shape { Shape::Noop => {} Shape::Vec(vec) => { @@ -1210,16 +1218,20 @@ impl Tessellator { self.tessellate_circle(circle, out); } Shape::Mesh(mesh) => { - if !mesh.is_valid() { + crate::profile_scope!("mesh"); + + if self.options.validate_meshes && !mesh.is_valid() { crate::epaint_assert!(false, "Invalid Mesh in Shape::Mesh"); return; } + // note: `append` still checks if the mesh is valid if extra asserts are enabled. if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(mesh.calc_bounds()) { return; } + out.append(mesh); } Shape::LineSegment { points, stroke } => self.tessellate_line(points, stroke, out), @@ -1362,6 +1374,8 @@ impl Tessellator { return; } + crate::profile_function!(); + let PathShape { points, closed, @@ -1474,6 +1488,7 @@ impl Tessellator { underline, override_text_color, fallback_color, + opacity_factor, angle, } = text_shape; @@ -1481,6 +1496,10 @@ impl Tessellator { return; } + if *opacity_factor <= 0.0 { + return; + } + if galley.pixels_per_point != self.pixels_per_point { eprintln!("epaint: WARNING: pixels_per_point (dpi scale) have changed between text layout and tessellation. \ You must recreate your text shapes if pixels_per_point changes."); @@ -1548,6 +1567,10 @@ impl Tessellator { color = *fallback_color; } + if *opacity_factor < 1.0 { + color = color.gamma_multiply(*opacity_factor); + } + crate::epaint_assert!(color != Color32::PLACEHOLDER, "A placeholder color made it to the tessellator. You forgot to set a fallback color."); let offset = if *angle == 0.0 { @@ -1665,21 +1688,7 @@ impl Tessellator { } } -/// Turns [`Shape`]:s into sets of triangles. -/// -/// The given shapes will tessellated in the same order as they are given. -/// They will be batched together by clip rectangle. -/// -/// * `pixels_per_point`: number of physical pixels to each logical point -/// * `options`: tessellation quality -/// * `shapes`: what to tessellate -/// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text. -/// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec. -/// -/// The implementation uses a [`Tessellator`]. -/// -/// ## Returns -/// A list of clip rectangles with matching [`Mesh`]. +#[deprecated = "Use `Tessellator::new(…).tessellate_shapes(…)` instead"] pub fn tessellate_shapes( pixels_per_point: f32, options: TessellationOptions, @@ -1687,67 +1696,146 @@ pub fn tessellate_shapes( prepared_discs: Vec, shapes: Vec, ) -> Vec { - let mut tessellator = - Tessellator::new(pixels_per_point, options, font_tex_size, prepared_discs); + Tessellator::new(pixels_per_point, options, font_tex_size, prepared_discs) + .tessellate_shapes(shapes) +} - let mut clipped_primitives: Vec = Vec::default(); +impl Tessellator { + /// Turns [`Shape`]:s into sets of triangles. + /// + /// The given shapes will tessellated in the same order as they are given. + /// They will be batched together by clip rectangle. + /// + /// * `pixels_per_point`: number of physical pixels to each logical point + /// * `options`: tessellation quality + /// * `shapes`: what to tessellate + /// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text. + /// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec. + /// + /// The implementation uses a [`Tessellator`]. + /// + /// ## Returns + /// A list of clip rectangles with matching [`Mesh`]. + #[allow(unused_mut)] + pub fn tessellate_shapes(&mut self, mut shapes: Vec) -> Vec { + crate::profile_function!(); + + #[cfg(feature = "rayon")] + if self.options.parallel_tessellation { + self.parallel_tessellation_of_large_shapes(&mut shapes); + } - for clipped_shape in shapes { - tessellator.tessellate_clipped_shape(clipped_shape, &mut clipped_primitives); - } + let mut clipped_primitives: Vec = Vec::default(); - if options.debug_paint_clip_rects { - clipped_primitives = add_clip_rects(&mut tessellator, clipped_primitives); - } + { + crate::profile_scope!("tessellate"); + for clipped_shape in shapes { + self.tessellate_clipped_shape(clipped_shape, &mut clipped_primitives); + } + } - if options.debug_ignore_clip_rects { - for clipped_primitive in &mut clipped_primitives { - clipped_primitive.clip_rect = Rect::EVERYTHING; + if self.options.debug_paint_clip_rects { + clipped_primitives = self.add_clip_rects(clipped_primitives); } - } - clipped_primitives.retain(|p| { - p.clip_rect.is_positive() - && match &p.primitive { - Primitive::Mesh(mesh) => !mesh.is_empty(), - Primitive::Callback(_) => true, + if self.options.debug_ignore_clip_rects { + for clipped_primitive in &mut clipped_primitives { + clipped_primitive.clip_rect = Rect::EVERYTHING; } - }); + } + + clipped_primitives.retain(|p| { + p.clip_rect.is_positive() + && match &p.primitive { + Primitive::Mesh(mesh) => !mesh.is_empty(), + Primitive::Callback(_) => true, + } + }); - for clipped_primitive in &clipped_primitives { - if let Primitive::Mesh(mesh) = &clipped_primitive.primitive { - crate::epaint_assert!(mesh.is_valid(), "Tessellator generated invalid Mesh"); + for clipped_primitive in &clipped_primitives { + if let Primitive::Mesh(mesh) = &clipped_primitive.primitive { + crate::epaint_assert!(mesh.is_valid(), "Tessellator generated invalid Mesh"); + } } + + clipped_primitives } - clipped_primitives -} + /// Find large shapes and throw them on the rayon thread pool, + /// then replace the original shape with their tessellated meshes. + #[cfg(feature = "rayon")] + fn parallel_tessellation_of_large_shapes(&self, shapes: &mut [ClippedShape]) { + crate::profile_function!(); -fn add_clip_rects( - tessellator: &mut Tessellator, - clipped_primitives: Vec, -) -> Vec { - tessellator.clip_rect = Rect::EVERYTHING; - let stroke = Stroke::new(2.0, Color32::from_rgb(150, 255, 150)); - - clipped_primitives - .into_iter() - .flat_map(|clipped_primitive| { - let mut clip_rect_mesh = Mesh::default(); - tessellator.tessellate_shape( - Shape::rect_stroke(clipped_primitive.clip_rect, 0.0, stroke), - &mut clip_rect_mesh, - ); + use rayon::prelude::*; + + // We only parallelize large/slow stuff, because each tessellation job + // will allocate a new Mesh, and so it creates a lot of extra memory framentation + // and callocations that is only worth it for large shapes. + fn should_parallelize(shape: &Shape) -> bool { + match shape { + Shape::Vec(shapes) => 4 < shapes.len() || shapes.iter().any(should_parallelize), - [ - clipped_primitive, - ClippedPrimitive { - clip_rect: Rect::EVERYTHING, // whatever - primitive: Primitive::Mesh(clip_rect_mesh), - }, - ] - }) - .collect() + Shape::Path(path_shape) => 32 < path_shape.points.len(), + + Shape::QuadraticBezier(_) | Shape::CubicBezier(_) => true, + + Shape::Noop + | Shape::Text(_) + | Shape::Circle(_) + | Shape::Mesh(_) + | Shape::LineSegment { .. } + | Shape::Rect(_) + | Shape::Callback(_) => false, + } + } + + let tessellated: Vec<(usize, Mesh)> = shapes + .par_iter() + .enumerate() + .filter(|(_, clipped_shape)| should_parallelize(&clipped_shape.shape)) + .map(|(index, clipped_shape)| { + crate::profile_scope!("tessellate_big_shape"); + // TODO: reuse tessellator in a thread local + let mut tessellator = (*self).clone(); + let mut mesh = Mesh::default(); + tessellator.tessellate_shape(clipped_shape.shape.clone(), &mut mesh); + (index, mesh) + }) + .collect(); + + crate::profile_scope!("distribute results", tessellated.len().to_string()); + for (index, mesh) in tessellated { + shapes[index].shape = Shape::Mesh(mesh); + } + } + + fn add_clip_rects( + &mut self, + clipped_primitives: Vec, + ) -> Vec { + self.clip_rect = Rect::EVERYTHING; + let stroke = Stroke::new(2.0, Color32::from_rgb(150, 255, 150)); + + clipped_primitives + .into_iter() + .flat_map(|clipped_primitive| { + let mut clip_rect_mesh = Mesh::default(); + self.tessellate_shape( + Shape::rect_stroke(clipped_primitive.clip_rect, 0.0, stroke), + &mut clip_rect_mesh, + ); + + [ + clipped_primitive, + ClippedPrimitive { + clip_rect: Rect::EVERYTHING, // whatever + primitive: Primitive::Mesh(clip_rect_mesh), + }, + ] + }) + .collect() + } } #[test] @@ -1776,12 +1864,8 @@ fn test_tessellator() { let font_tex_size = [1024, 1024]; // unused let prepared_discs = vec![]; // unused - let primitives = tessellate_shapes( - 1.0, - Default::default(), - font_tex_size, - prepared_discs, - clipped_shapes, - ); + let primitives = Tessellator::new(1.0, Default::default(), font_tex_size, prepared_discs) + .tessellate_shapes(clipped_shapes); + assert_eq!(primitives.len(), 2); } diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index c27ae5a105e..b3fda5a5810 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -44,7 +44,7 @@ impl eframe::App for MyApp { .labelled_by(name_label.id); }); ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); - if ui.button("Click each year").clicked() { + if ui.button("Increment").clicked() { self.age += 1; } ui.label(format!("Hello '{}', age {}", self.name, self.age)); diff --git a/examples/hello_world_par/src/main.rs b/examples/hello_world_par/src/main.rs index 604939ff10f..617e840dc4c 100644 --- a/examples/hello_world_par/src/main.rs +++ b/examples/hello_world_par/src/main.rs @@ -49,7 +49,7 @@ impl ThreadState { ui.text_edit_singleline(&mut self.name); }); ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); - if ui.button("Click each year").clicked() { + if ui.button("Increment").clicked() { self.age += 1; } ui.label(format!("Hello '{}', age {}", self.name, self.age)); diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 80e263f4b3d..5f0ed31a49f 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -23,7 +23,7 @@ fn main() -> Result<(), eframe::Error> { .labelled_by(name_label.id); }); ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); - if ui.button("Click each year").clicked() { + if ui.button("Increment").clicked() { age += 1; } ui.label(format!("Hello '{name}', age {age}")); diff --git a/media/demo.gif b/media/demo.gif index 385c8228131..2df2ed0800f 100644 Binary files a/media/demo.gif and b/media/demo.gif differ diff --git a/scripts/check.sh b/scripts/check.sh index 542b738eccf..c1852a2d81a 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -35,10 +35,23 @@ cargo test --quiet --all-targets --all-features cargo test --quiet --doc # slow - checks all doc-tests cargo check --quiet -p eframe --no-default-features --features "glow" -cargo check --quiet -p eframe --no-default-features --features "wgpu" +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + cargo check --quiet -p eframe --no-default-features --features "wgpu","x11" + cargo check --quiet -p eframe --no-default-features --features "wgpu","wayland" +else + cargo check --quiet -p eframe --no-default-features --features "wgpu" +fi + cargo check --quiet -p egui --no-default-features --features "serde" cargo check --quiet -p egui_demo_app --no-default-features --features "glow" -cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu" + +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu","x11" + cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu","wayland" +else + cargo check --quiet -p egui_demo_app --no-default-features --features "wgpu" +fi + cargo check --quiet -p egui_demo_lib --no-default-features cargo check --quiet -p egui_extras --no-default-features cargo check --quiet -p egui_glow --no-default-features