diff --git a/README.md b/README.md index 5008c036..9d08ef5a 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,7 @@ These are the official egui integrations: * [`amethyst_egui`](https://github.com/jgraef/amethyst_egui) for [the Amethyst game engine](https://amethyst.rs/). * [`bevy_egui`](https://github.com/mvlabat/bevy_egui) for [the Bevy game engine](https://bevyengine.org/). * [`egui_glfw_gl`](https://github.com/cohaereo/egui_glfw_gl) for [GLFW](https://crates.io/crates/glfw). +* [`egui-glutin-gl`](https://github.com/h3r2tic/egui-glutin-gl/) for [glutin](https://crates.io/crates/glutin). * [`egui_sdl2_gl`](https://crates.io/crates/egui_sdl2_gl) for [SDL2](https://crates.io/crates/sdl2). * [`egui_sdl2_platform`](https://github.com/ComLarsic/egui_sdl2_platform) for [SDL2](https://crates.io/crates/sdl2). * [`egui_vulkano`](https://github.com/derivator/egui_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano). diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 278f5a17..b3cd2518 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -270,6 +270,10 @@ pub struct NativeOptions { /// You should avoid having a [`egui::CentralPanel`], or make sure its frame is also transparent. pub transparent: bool, + /// On desktop: mouse clicks pass through the window, used for non-interactable overlays + /// Generally you would use this in conjunction with always_on_top + pub mouse_passthrough: bool, + /// Turn on vertical syncing, limiting the FPS to the display refresh rate. /// /// The default is `true`. @@ -389,6 +393,7 @@ impl Default for NativeOptions { max_window_size: None, resizable: true, transparent: false, + mouse_passthrough: false, vsync: true, multisampling: 0, depth_buffer: 0, diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 0266c731..a248ad76 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -383,6 +383,9 @@ mod glow_integration { integration.egui_ctx.set_visuals(theme.egui_visuals()); gl_window.window().set_ime_allowed(true); + if self.native_options.mouse_passthrough { + gl_window.window().set_cursor_hittest(false).unwrap(); + } { let event_loop_proxy = self.repaint_proxy.clone(); diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 0a317d4c..6a5b760d 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -244,6 +244,13 @@ impl Prepared { rect } + fn content_with_margin(&self) -> Rect { + let mut rect = self.content_ui.min_rect(); + rect.min -= self.frame.inner_margin.left_top() + self.frame.outer_margin.left_top(); + rect.max += self.frame.inner_margin.right_bottom() + self.frame.outer_margin.right_bottom(); + rect + } + pub fn end(self, ui: &mut Ui) -> Response { let paint_rect = self.paint_rect(); @@ -258,6 +265,6 @@ impl Prepared { ui.painter().set(where_to_put_background, shape); } - ui.allocate_rect(paint_rect, Sense::hover()) + ui.allocate_rect(self.content_with_margin(), Sense::hover()) } } diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 1f5ab1be..a517db1e 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -73,25 +73,15 @@ impl Default for CoordinatesFormatter { const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[derive(Clone)] -struct AutoBounds { +#[derive(Copy, Clone)] +struct AxisBools { x: bool, y: bool, } -impl AutoBounds { - fn from_bool(val: bool) -> Self { - AutoBounds { x: val, y: val } - } - - fn any(&self) -> bool { - self.x || self.y - } -} - -impl From for AutoBounds { +impl From for AxisBools { fn from(val: bool) -> Self { - AutoBounds::from_bool(val) + AxisBools { x: val, y: val } } } @@ -99,10 +89,11 @@ impl From for AutoBounds { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] struct PlotMemory { - auto_bounds: AutoBounds, + /// Indicates if the user has modified the bounds, for example by moving or zooming, + /// or if the bounds should be calculated based by included point or auto bounds. + bounds_modified: AxisBools, hovered_entry: Option, hidden_items: ahash::HashSet, - min_auto_bounds: PlotBounds, last_screen_transform: ScreenTransform, /// Allows to remember the first click position when performing a boxed zoom last_click_pos_for_zoom: Option, @@ -268,6 +259,7 @@ pub struct Plot { allow_zoom: bool, allow_drag: bool, allow_scroll: bool, + auto_bounds: AxisBools, min_auto_bounds: PlotBounds, margin_fraction: Vec2, allow_boxed_zoom: bool, @@ -281,6 +273,8 @@ pub struct Plot { data_aspect: Option, view_aspect: Option, + reset: bool, + show_x: bool, show_y: bool, label_formatter: LabelFormatter, @@ -303,6 +297,7 @@ impl Plot { allow_zoom: true, allow_drag: true, allow_scroll: true, + auto_bounds: false.into(), min_auto_bounds: PlotBounds::NOTHING, margin_fraction: Vec2::splat(0.05), allow_boxed_zoom: true, @@ -316,6 +311,8 @@ impl Plot { data_aspect: None, view_aspect: None, + reset: false, + show_x: true, show_y: true, label_formatter: None, @@ -402,7 +399,7 @@ impl Plot { self } - /// Set the side margin as a fraction of the plot size. + /// Set the side margin as a fraction of the plot size. Only used for auto bounds. /// /// For instance, a value of `0.1` will add 10% space on both sides. pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self { @@ -556,6 +553,18 @@ impl Plot { self } + /// Expand bounds to fit all items across the x axis, including values given by `include_x`. + pub fn auto_bounds_x(mut self) -> Self { + self.auto_bounds.x = true; + self + } + + /// Expand bounds to fit all items across the y axis, including values given by `include_y`. + pub fn auto_bounds_y(mut self) -> Self { + self.auto_bounds.y = true; + self + } + /// Show a legend including all named items. pub fn legend(mut self, legend: Legend) -> Self { self.legend_config = Some(legend); @@ -592,6 +601,12 @@ impl Plot { self } + /// Resets the plot. + pub fn reset(mut self) -> Self { + self.reset = true; + self + } + /// Interact with and add items to the plot and finally draw it. pub fn show(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse { self.show_dyn(ui, Box::new(build_fn)) @@ -611,6 +626,7 @@ impl Plot { allow_drag, allow_boxed_zoom, boxed_zoom_pointer_button: boxed_zoom_pointer, + auto_bounds, min_auto_bounds, margin_fraction, width, @@ -624,6 +640,7 @@ impl Plot { coordinates_formatter, axis_formatters, legend_config, + reset, show_background, show_axes, linked_axes, @@ -661,11 +678,19 @@ impl Plot { // Load or initialize the memory. let plot_id = ui.make_persistent_id(id_source); ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); - let mut memory = PlotMemory::load(ui.ctx(), plot_id).unwrap_or_else(|| PlotMemory { - auto_bounds: (!min_auto_bounds.is_valid()).into(), + let memory = if reset { + if let Some(axes) = linked_axes.as_ref() { + axes.bounds.set(None); + }; + + None + } else { + PlotMemory::load(ui.ctx(), plot_id) + } + .unwrap_or_else(|| PlotMemory { + bounds_modified: false.into(), hovered_entry: None, hidden_items: Default::default(), - min_auto_bounds, last_screen_transform: ScreenTransform::new( rect, min_auto_bounds, @@ -675,24 +700,12 @@ impl Plot { last_click_pos_for_zoom: None, }); - // If the min bounds changed, recalculate everything. - if min_auto_bounds != memory.min_auto_bounds { - memory = PlotMemory { - auto_bounds: (!min_auto_bounds.is_valid()).into(), - hovered_entry: None, - min_auto_bounds, - ..memory - }; - memory.clone().store(ui.ctx(), plot_id); - } - let PlotMemory { - mut auto_bounds, + mut bounds_modified, mut hovered_entry, mut hidden_items, last_screen_transform, mut last_click_pos_for_zoom, - .. } = memory; // Call the plot build function. @@ -774,52 +787,51 @@ impl Plot { if let Some(linked_bounds) = axes.get() { if axes.link_x { bounds.set_x(&linked_bounds); - // Turn off auto bounds to keep it from overriding what we just set. - auto_bounds.x = false; + // Mark the axis as modified to prevent it from being changed. + bounds_modified.x = true; } if axes.link_y { bounds.set_y(&linked_bounds); - // Turn off auto bounds to keep it from overriding what we just set. - auto_bounds.y = false; + // Mark the axis as modified to prevent it from being changed. + bounds_modified.y = true; } } }; - // Allow double clicking to reset to automatic bounds. + // Allow double clicking to reset to the initial bounds. if response.double_clicked_by(PointerButton::Primary) { - auto_bounds = true.into(); + bounds_modified = false.into(); } - if !bounds.is_valid() { - auto_bounds = true.into(); + // Reset bounds to initial bounds if we haven't been modified. + if !bounds_modified.x { + bounds.set_x(&min_auto_bounds); } + if !bounds_modified.y { + bounds.set_y(&min_auto_bounds); + } + + let auto_x = !bounds_modified.x && (!min_auto_bounds.is_valid_x() || auto_bounds.x); + let auto_y = !bounds_modified.y && (!min_auto_bounds.is_valid_y() || auto_bounds.y); // Set bounds automatically based on content. - if auto_bounds.any() { - if auto_bounds.x { - bounds.set_x(&min_auto_bounds); - } - - if auto_bounds.y { - bounds.set_y(&min_auto_bounds); - } - + if auto_x || auto_y { for item in &items { let item_bounds = item.bounds(); - if auto_bounds.x { + if auto_x { bounds.merge_x(&item_bounds); } - if auto_bounds.y { + if auto_y { bounds.merge_y(&item_bounds); } } - if auto_bounds.x { + if auto_x { bounds.add_relative_margin_x(margin_fraction); } - if auto_bounds.y { + if auto_y { bounds.add_relative_margin_y(margin_fraction); } } @@ -842,7 +854,7 @@ impl Plot { if allow_drag && response.dragged_by(PointerButton::Primary) { response = response.on_hover_cursor(CursorIcon::Grabbing); transform.translate_bounds(-response.drag_delta()); - auto_bounds = false.into(); + bounds_modified = true.into(); } // Zooming @@ -889,7 +901,7 @@ impl Plot { }; if new_bounds.is_valid() { transform.set_bounds(new_bounds); - auto_bounds = false.into(); + bounds_modified = true.into(); } // reset the boxed zoom state last_click_pos_for_zoom = None; @@ -906,14 +918,14 @@ impl Plot { }; if zoom_factor != Vec2::splat(1.0) { transform.zoom(zoom_factor, hover_pos); - auto_bounds = false.into(); + bounds_modified = true.into(); } } if allow_scroll { let scroll_delta = ui.input().scroll_delta; if scroll_delta != Vec2::ZERO { transform.translate_bounds(-scroll_delta); - auto_bounds = false.into(); + bounds_modified = true.into(); } } } @@ -963,10 +975,9 @@ impl Plot { } let memory = PlotMemory { - auto_bounds, + bounds_modified, hovered_entry, hidden_items, - min_auto_bounds, last_screen_transform: transform, last_click_pos_for_zoom, }; diff --git a/crates/egui/src/widgets/plot/transform.rs b/crates/egui/src/widgets/plot/transform.rs index 584d0a0a..6abfe261 100644 --- a/crates/egui/src/widgets/plot/transform.rs +++ b/crates/egui/src/widgets/plot/transform.rs @@ -40,10 +40,26 @@ impl PlotBounds { && self.max[1].is_finite() } + pub fn is_finite_x(&self) -> bool { + self.min[0].is_finite() && self.max[0].is_finite() + } + + pub fn is_finite_y(&self) -> bool { + self.min[1].is_finite() && self.max[1].is_finite() + } + pub fn is_valid(&self) -> bool { self.is_finite() && self.width() > 0.0 && self.height() > 0.0 } + pub fn is_valid_x(&self) -> bool { + self.is_finite_x() && self.width() > 0.0 + } + + pub fn is_valid_y(&self) -> bool { + self.is_finite_y() && self.height() > 0.0 + } + pub fn width(&self) -> f64 { self.max[0] - self.min[0] } @@ -181,8 +197,11 @@ pub(crate) struct ScreenTransform { impl ScreenTransform { pub fn new(frame: Rect, mut bounds: PlotBounds, x_centered: bool, y_centered: bool) -> Self { // Make sure they are not empty. - if !bounds.is_valid() { - bounds = PlotBounds::new_symmetrical(1.0); + if !bounds.is_valid_x() { + bounds.set_x(&PlotBounds::new_symmetrical(1.0)); + } + if !bounds.is_valid_y() { + bounds.set_y(&PlotBounds::new_symmetrical(1.0)); } // Scale axes so that the origin is in the center. diff --git a/crates/egui/src/widgets/spinner.rs b/crates/egui/src/widgets/spinner.rs index b2ce0d43..73762b44 100644 --- a/crates/egui/src/widgets/spinner.rs +++ b/crates/egui/src/widgets/spinner.rs @@ -1,4 +1,4 @@ -use epaint::{emath::lerp, vec2, Pos2, Shape, Stroke}; +use epaint::{emath::lerp, vec2, Color32, Pos2, Shape, Stroke}; use crate::{Response, Sense, Ui, Widget}; @@ -10,6 +10,7 @@ use crate::{Response, Sense, Ui, Widget}; pub struct Spinner { /// Uses the style's `interact_size` if `None`. size: Option, + color: Option, } impl Spinner { @@ -24,6 +25,12 @@ impl Spinner { self.size = Some(size); self } + + /// Sets the spinner's color. + pub fn color(mut self, color: impl Into) -> Self { + self.color = Some(color.into()); + self + } } impl Widget for Spinner { @@ -31,6 +38,9 @@ impl Widget for Spinner { let size = self .size .unwrap_or_else(|| ui.style().spacing.interact_size.y); + let color = self + .color + .unwrap_or_else(|| ui.visuals().strong_text_color()); let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); if ui.is_rect_visible(rect) { @@ -47,10 +57,8 @@ impl Widget for Spinner { rect.center() + radius * vec2(cos as f32, sin as f32) }) .collect(); - ui.painter().add(Shape::line( - points, - Stroke::new(3.0, ui.visuals().strong_text_color()), - )); + ui.painter() + .add(Shape::line(points, Stroke::new(3.0, color))); } response