diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fd008ba..bbdb2883 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,13 +9,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
+### Added ⭐
+
+* Added `egui::plot::Plot` to plot some 2D data
+
## 0.9.0 - 2021-02-07 - Light Mode and much more
-### Added ⭐
-
+### Added ⭐
+
* Add support for secondary and middle mouse buttons.
* Add `Label` methods for code, strong, strikethrough, underline and italics.
* Add `ui.group(|ui| { … })` to visually group some widgets within a frame.
diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs
index dab8bccb..32119867 100644
--- a/egui/src/input_state.rs
+++ b/egui/src/input_state.rs
@@ -31,7 +31,9 @@ pub struct InputState {
pub time: f64,
/// Time since last frame, in seconds.
- /// This can be very unstable in reactive mode (when we don't paint each frame).
+ ///
+ /// This can be very unstable in reactive mode (when we don't paint each frame)
+ /// so it can be smart ot use e.g. `unstable_dt.min(1.0 / 30.0)`.
pub unstable_dt: f32,
/// Used for animations to get instant feedback (avoid frame delay).
diff --git a/egui/src/lib.rs b/egui/src/lib.rs
index 1529be17..07d8ed2f 100644
--- a/egui/src/lib.rs
+++ b/egui/src/lib.rs
@@ -51,8 +51,32 @@
//! /* take some action here */
//! }
//! });
+//! ```
//!
//!
+//! # Code snippets
+//!
+//! ```
+//! # let ui = &mut egui::Ui::__test();
+//! # let mut some_bool = true;
+//! // Some examples, tips and tricks, etc.
+//!
+//! ui.checkbox(&mut some_bool, "Click to toggle");
+//!
+//! ui.horizontal(|ui|{
+//! // `radio_value` also works for enums, integers, and more.
+//! ui.radio_value(&mut some_bool, false, "Off");
+//! ui.radio_value(&mut some_bool, true, "On");
+//! });
+//!
+//! if ui.button("Click me!").clicked() { }
+//!
+//! // Change test color on subsequent widgets:
+//! ui.visuals_mut().override_text_color = Some(egui::Color32::RED);
+//!
+//! // Turn off text wrapping on subsequent widgets:
+//! ui.style_mut().wrap = Some(false);
+//! ```
#![cfg_attr(not(debug_assertions), deny(warnings))] // Forbid warnings in release builds
#![forbid(unsafe_code)]
diff --git a/egui/src/style.rs b/egui/src/style.rs
index f26f1e76..8e25a32e 100644
--- a/egui/src/style.rs
+++ b/egui/src/style.rs
@@ -16,6 +16,10 @@ pub struct Style {
/// If set, labels buttons wtc will use this to determine whether or not
/// to wrap the text at the right edge of the `Ui` they are in.
/// By default this is `None`.
+ ///
+ /// * `None`: follow layout
+ /// * `Some(true)`: default on
+ /// * `Some(false)`: default off
pub wrap: Option,
pub spacing: Spacing,
diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs
index 301f7372..fe1c3fd9 100644
--- a/egui/src/widgets/mod.rs
+++ b/egui/src/widgets/mod.rs
@@ -14,6 +14,7 @@ mod drag_value;
mod hyperlink;
mod image;
mod label;
+pub mod plot;
mod selected_label;
mod separator;
mod slider;
diff --git a/egui/src/widgets/plot.rs b/egui/src/widgets/plot.rs
new file mode 100644
index 00000000..3eb8b9cd
--- /dev/null
+++ b/egui/src/widgets/plot.rs
@@ -0,0 +1,759 @@
+//! Simple plotting library.
+
+#![allow(clippy::comparison_chain)]
+
+use crate::*;
+
+// ----------------------------------------------------------------------------
+
+/// A value in the value-space of the plot.
+///
+/// Uses f64 for improved accuracy to enable plotting
+/// large values (e.g. unix time on x axis).
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct Value {
+ /// This is often something monotonically increasing, such as time, but doesn't have to be.
+ /// Goes from left to right.
+ pub x: f64,
+ /// Goes from bottom to top (inverse of everything else in egui!).
+ pub y: f64,
+}
+
+impl Value {
+ pub fn new(x: impl Into, y: impl Into) -> Self {
+ Self {
+ x: x.into(),
+ y: y.into(),
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+/// 2D bounding box of f64 precision.
+/// The range of data values we show.
+#[derive(Clone, Copy, PartialEq)]
+struct Bounds {
+ min: [f64; 2],
+ max: [f64; 2],
+}
+
+impl Bounds {
+ pub const NOTHING: Self = Self {
+ min: [f64::INFINITY; 2],
+ max: [-f64::INFINITY; 2],
+ };
+
+ pub fn width(&self) -> f64 {
+ self.max[0] - self.min[0]
+ }
+
+ pub fn height(&self) -> f64 {
+ self.max[1] - self.min[1]
+ }
+
+ pub fn is_finite(&self) -> bool {
+ self.min[0].is_finite()
+ && self.min[1].is_finite()
+ && self.max[0].is_finite()
+ && self.max[1].is_finite()
+ }
+
+ pub fn extend_with(&mut self, value: &Value) {
+ self.extend_with_x(value.x);
+ self.extend_with_y(value.y);
+ }
+
+ /// Expand to include the given x coordinate
+ pub fn extend_with_x(&mut self, x: f64) {
+ self.min[0] = self.min[0].min(x);
+ self.max[0] = self.max[0].max(x);
+ }
+
+ /// Expand to include the given y coordinate
+ pub fn extend_with_y(&mut self, y: f64) {
+ self.min[1] = self.min[1].min(y);
+ self.max[1] = self.max[1].max(y);
+ }
+
+ pub fn expand_x(&mut self, pad: f64) {
+ self.min[0] -= pad;
+ self.max[0] += pad;
+ }
+
+ pub fn expand_y(&mut self, pad: f64) {
+ self.min[1] -= pad;
+ self.max[1] += pad;
+ }
+
+ pub fn union_mut(&mut self, other: &Bounds) {
+ self.min[0] = self.min[0].min(other.min[0]);
+ self.min[1] = self.min[1].min(other.min[1]);
+ self.max[0] = self.max[0].max(other.max[0]);
+ self.max[1] = self.max[1].max(other.max[1]);
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+/// A horizontal line in a plot, filling the full width
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct HLine {
+ y: f64,
+ stroke: Stroke,
+}
+
+impl HLine {
+ pub fn new(y: impl Into, stroke: impl Into) -> Self {
+ Self {
+ y: y.into(),
+ stroke: stroke.into(),
+ }
+ }
+}
+
+/// A vertical line in a plot, filling the full width
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct VLine {
+ x: f64,
+ stroke: Stroke,
+}
+
+impl VLine {
+ pub fn new(x: impl Into, stroke: impl Into) -> Self {
+ Self {
+ x: x.into(),
+ stroke: stroke.into(),
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+/// A series of values forming a path.
+#[derive(Clone, PartialEq)]
+pub struct Curve {
+ values: Vec,
+ bounds: Bounds,
+ stroke: Stroke,
+ name: String,
+}
+
+impl Curve {
+ pub fn from_values(values: Vec) -> Self {
+ let mut bounds = Bounds::NOTHING;
+ for value in &values {
+ bounds.extend_with(value);
+ }
+ Self {
+ values,
+ bounds,
+ stroke: Stroke::new(2.0, Color32::from_gray(120)),
+ name: Default::default(),
+ }
+ }
+
+ pub fn from_values_iter(iter: impl Iterator- ) -> Self {
+ Self::from_values(iter.collect())
+ }
+
+ /// From a series of y-values.
+ /// The x-values will be the indices of these values
+ pub fn from_ys_f32(ys: &[f32]) -> Self {
+ let values: Vec = ys
+ .iter()
+ .enumerate()
+ .map(|(i, &y)| Value {
+ x: i as f64,
+ y: y as f64,
+ })
+ .collect();
+ Self::from_values(values)
+ }
+
+ pub fn stroke(mut self, stroke: impl Into) -> Self {
+ self.stroke = stroke.into();
+ self
+ }
+
+ /// Stroke width (in points).
+ pub fn width(mut self, width: f32) -> Self {
+ self.stroke.width = width;
+ self
+ }
+
+ /// Stroke color.
+ pub fn color(mut self, color: impl Into) -> Self {
+ self.stroke.color = color.into();
+ self
+ }
+
+ /// Name of this curve.
+ pub fn name(mut self, name: impl Into) -> Self {
+ self.name = name.into();
+ self
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+/// A 2D plot, e.g. a graph of a function.
+///
+/// `Plot` supports multiple curves.
+///
+/// ```
+/// # let ui = &mut egui::Ui::__test();
+/// use egui::Color32;
+/// use egui::plot::{Curve, Plot, Value};
+/// let sin = (0..1000).map(|i| {
+/// let x = i as f64 * 0.01;
+/// Value::new(x, x.sin())
+/// });
+/// let curve = Curve::from_values_iter(sin).color(Color32::from_rgb(50, 200, 50));
+/// ui.add(
+/// Plot::default().curve(curve).view_aspect(2.0)
+/// );
+/// ```
+#[derive(Clone, PartialEq)]
+pub struct Plot {
+ curves: Vec,
+ hlines: Vec,
+ vlines: Vec,
+
+ bounds: Bounds,
+ symmetrical_x_bounds: bool,
+ symmetrical_y_bounds: bool,
+ margin_fraction: Vec2,
+
+ min_size: Vec2,
+ width: Option,
+ height: Option,
+ data_aspect: Option,
+ view_aspect: Option,
+
+ show_x: bool,
+ show_y: bool,
+}
+
+impl Default for Plot {
+ fn default() -> Self {
+ Self {
+ curves: Default::default(),
+ hlines: Default::default(),
+ vlines: Default::default(),
+
+ bounds: Bounds::NOTHING,
+ symmetrical_x_bounds: false,
+ symmetrical_y_bounds: false,
+ margin_fraction: Vec2::splat(0.05),
+
+ min_size: Vec2::splat(64.0),
+ width: None,
+ height: None,
+ data_aspect: None,
+ view_aspect: None,
+
+ show_x: true,
+ show_y: true,
+ }
+ }
+}
+
+impl Plot {
+ /// Add a data curve.
+ /// You can add multiple curves.
+ pub fn curve(mut self, curve: Curve) -> Self {
+ self.bounds.union_mut(&curve.bounds);
+ self.curves.push(curve);
+ self
+ }
+
+ /// Add a horizontal line.
+ /// Can be useful e.g. to show min/max bounds or similar.
+ /// Always fills the full width of the plot.
+ pub fn hline(mut self, hline: HLine) -> Self {
+ self = self.include_y(hline.y);
+ self.hlines.push(hline);
+ self
+ }
+
+ /// Add a vertical line.
+ /// Can be useful e.g. to show min/max bounds or similar.
+ /// Always fills the full height of the plot.
+ pub fn vline(mut self, vline: VLine) -> Self {
+ self = self.include_x(vline.x);
+ self.vlines.push(vline);
+ self
+ }
+
+ /// Expand bounds to include the given x value.
+ pub fn include_x(mut self, x: impl Into) -> Self {
+ self.bounds.extend_with_x(x.into());
+ self
+ }
+
+ /// Expand bounds to include the given y value.
+ /// For instance, to always show the x axis, call `plot.include_y(0.0)`.
+ pub fn include_y(mut self, y: impl Into) -> Self {
+ self.bounds.extend_with_y(y.into());
+ self
+ }
+
+ /// If true, the x-bounds will be symmetrical, so that the x=0 zero line
+ /// is always in the center.
+ pub fn symmetrical_x_bounds(mut self, symmetrical_x_bounds: bool) -> Self {
+ self.symmetrical_x_bounds = symmetrical_x_bounds;
+ self
+ }
+
+ /// If true, the y-bounds will be symmetrical, so that the y=0 zero line
+ /// is always in the center.
+ pub fn symmetrical_y_bounds(mut self, symmetrical_y_bounds: bool) -> Self {
+ self.symmetrical_y_bounds = symmetrical_y_bounds;
+ self
+ }
+
+ /// width / height ratio of the data.
+ /// For instance, it can be useful to set this to `1.0` for when the two axes show the same unit.
+ pub fn data_aspect(mut self, data_aspect: f32) -> Self {
+ self.data_aspect = Some(data_aspect);
+ self
+ }
+
+ /// width / height ratio of the plot region.
+ /// By default no fixed aspect ratio is set (and width/height will fill the ui it is in).
+ pub fn view_aspect(mut self, view_aspect: f32) -> Self {
+ self.view_aspect = Some(view_aspect);
+ self
+ }
+
+ /// Width of plot. By default a plot will fill the ui it is in.
+ /// If you set [`Self::view_aspect`], the width can be calculated from the height.
+ pub fn width(mut self, width: f32) -> Self {
+ self.width = Some(width);
+ self
+ }
+
+ /// Height of plot. By default a plot will fill the ui it is in.
+ /// If you set [`Self::view_aspect`], the height can be calculated from the width.
+ pub fn height(mut self, height: f32) -> Self {
+ self.height = Some(height);
+ self
+ }
+
+ /// Minimum size of the plot view.
+ pub fn min_size(mut self, min_size: Vec2) -> Self {
+ self.min_size = min_size;
+ self
+ }
+
+ /// Show the x-value (e.g. when hovering). Default: `true`.
+ pub fn show_x(mut self, show_x: bool) -> Self {
+ self.show_x = show_x;
+ self
+ }
+
+ /// Show the y-value (e.g. when hovering). Default: `true`.
+ pub fn show_y(mut self, show_y: bool) -> Self {
+ self.show_y = show_y;
+ self
+ }
+}
+
+impl Widget for Plot {
+ fn ui(self, ui: &mut Ui) -> Response {
+ let Self {
+ curves,
+ hlines,
+ vlines,
+ bounds,
+ symmetrical_x_bounds,
+ symmetrical_y_bounds,
+ margin_fraction,
+ width,
+ height,
+ min_size,
+ data_aspect,
+ view_aspect,
+ show_x,
+ show_y,
+ } = self;
+
+ let size = {
+ let width = width.map(|w| w.at_least(min_size.x));
+ let height = height.map(|w| w.at_least(min_size.y));
+
+ let width = width.unwrap_or_else(|| {
+ if let (Some(height), Some(aspect)) = (height, view_aspect) {
+ height * aspect
+ } else {
+ ui.available_size_before_wrap_finite().x
+ }
+ });
+ let width = width.at_least(min_size.x);
+
+ let height = height.unwrap_or_else(|| {
+ if let Some(aspect) = view_aspect {
+ width / aspect
+ } else {
+ ui.available_size_before_wrap_finite().y
+ }
+ });
+ let height = height.at_least(min_size.y);
+ vec2(width, height)
+ };
+
+ let (rect, response) = ui.allocate_exact_size(size, Sense::hover());
+
+ let mut bounds = bounds;
+
+ if symmetrical_x_bounds {
+ let x_abs = bounds.min[0].abs().max(bounds.max[0].abs());
+ bounds.min[0] = -x_abs;
+ bounds.max[0] = x_abs;
+ };
+ if symmetrical_y_bounds {
+ let y_abs = bounds.min[1].abs().max(bounds.max[1].abs());
+ bounds.min[1] = -y_abs;
+ bounds.max[1] = y_abs;
+ };
+
+ bounds.expand_x(margin_fraction.x as f64 * bounds.width());
+ bounds.expand_y(margin_fraction.y as f64 * bounds.height());
+
+ if let Some(data_aspect) = data_aspect {
+ let data_aspect = data_aspect as f64;
+ let rw = rect.width() as f64;
+ let rh = rect.height() as f64;
+ let current_data_aspect = (bounds.width() / rw) / (bounds.height() / rh);
+ if current_data_aspect < data_aspect {
+ bounds.expand_x((data_aspect / current_data_aspect - 1.0) * bounds.width() * 0.5);
+ } else {
+ bounds.expand_y((current_data_aspect / data_aspect - 1.0) * bounds.height() * 0.5);
+ }
+ }
+
+ // Background:
+ ui.painter().add(Shape::Rect {
+ rect,
+ corner_radius: 2.0,
+ fill: ui.visuals().extreme_bg_color,
+ stroke: ui.visuals().window_stroke(),
+ });
+
+ if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 {
+ let prepared = Prepared {
+ curves,
+ hlines,
+ vlines,
+ rect,
+ bounds,
+ show_x,
+ show_y,
+ };
+ prepared.ui(ui, &response);
+ }
+
+ response
+ }
+}
+
+struct Prepared {
+ curves: Vec,
+ hlines: Vec,
+ vlines: Vec,
+ /// Screen space position of the plot
+ rect: Rect,
+ bounds: Bounds,
+ show_x: bool,
+ show_y: bool,
+}
+
+impl Prepared {
+ fn position_from_value(&self, value: &Value) -> Pos2 {
+ let x = remap(
+ value.x,
+ self.bounds.min[0]..=self.bounds.max[0],
+ (self.rect.left() as f64)..=(self.rect.right() as f64),
+ );
+ let y = remap(
+ value.y,
+ self.bounds.min[1]..=self.bounds.max[1],
+ (self.rect.bottom() as f64)..=(self.rect.top() as f64), // negated y axis!
+ );
+ pos2(x as f32, y as f32)
+ }
+
+ /// delta position / delta value
+ fn dpos_dvalue_x(&self) -> f64 {
+ self.rect.width() as f64 / self.bounds.width()
+ }
+
+ /// delta position / delta value
+ fn dpos_dvalue_y(&self) -> f64 {
+ -self.rect.height() as f64 / self.bounds.height() // negated y axis!
+ }
+
+ /// delta position / delta value
+ fn dpos_dvalue(&self) -> [f64; 2] {
+ [self.dpos_dvalue_x(), self.dpos_dvalue_y()]
+ }
+
+ /// delta value / delta position
+ fn dvalue_dpos(&self) -> [f64; 2] {
+ [1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
+ }
+
+ fn value_from_position(&self, pos: Pos2) -> Value {
+ let x = remap(
+ pos.x as f64,
+ (self.rect.left() as f64)..=(self.rect.right() as f64),
+ self.bounds.min[0]..=self.bounds.max[0],
+ );
+ let y = remap(
+ pos.y as f64,
+ (self.rect.bottom() as f64)..=(self.rect.top() as f64), // negated y axis!
+ self.bounds.min[1]..=self.bounds.max[1],
+ );
+ Value::new(x, y)
+ }
+
+ fn ui(&self, ui: &mut Ui, response: &Response) {
+ let mut shapes = Vec::with_capacity(self.hlines.len() + self.curves.len() + 2);
+
+ for d in 0..2 {
+ self.paint_axis(ui, d, &mut shapes);
+ }
+
+ for &hline in &self.hlines {
+ let HLine { y, stroke } = hline;
+ let points = [
+ self.position_from_value(&Value::new(self.bounds.min[0], y)),
+ self.position_from_value(&Value::new(self.bounds.max[0], y)),
+ ];
+ shapes.push(Shape::line_segment(points, stroke));
+ }
+
+ for &vline in &self.vlines {
+ let VLine { x, stroke } = vline;
+ let points = [
+ self.position_from_value(&Value::new(x, self.bounds.min[1])),
+ self.position_from_value(&Value::new(x, self.bounds.max[1])),
+ ];
+ shapes.push(Shape::line_segment(points, stroke));
+ }
+
+ for curve in &self.curves {
+ let stroke = curve.stroke;
+ let values = &curve.values;
+ if values.len() == 1 {
+ let point = self.position_from_value(&values[0]);
+ shapes.push(Shape::circle_filled(
+ point,
+ stroke.width / 2.0,
+ stroke.color,
+ ));
+ } else if values.len() > 1 {
+ shapes.push(Shape::line(
+ values.iter().map(|v| self.position_from_value(v)).collect(),
+ stroke,
+ ));
+ }
+ }
+
+ if response.hovered() {
+ if let Some(pointer) = ui.input().pointer.tooltip_pos() {
+ self.hover(ui, pointer, &mut shapes);
+ }
+ }
+
+ ui.painter().sub_region(self.rect).extend(shapes);
+ }
+
+ fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) {
+ let bounds = self.bounds;
+ let text_style = TextStyle::Body;
+
+ let base: f64 = 10.0;
+
+ let min_label_spacing_in_points = 60.0; // TODO: large enough for a wide label
+ let step_size = self.dvalue_dpos()[axis] * min_label_spacing_in_points;
+ let step_size = base.powi(step_size.abs().log(base).ceil() as i32);
+
+ let step_size_in_points = (self.dpos_dvalue()[axis] * step_size) as f32;
+
+ // Where on the cross-dimension to show the label values
+ let value_cross = clamp(0.0, bounds.min[1 - axis]..=bounds.max[1 - axis]);
+
+ for i in 0.. {
+ let value_main = step_size * (bounds.min[axis] / step_size + i as f64).floor();
+ if value_main > bounds.max[axis] {
+ break;
+ }
+
+ let value = if axis == 0 {
+ Value::new(value_main, value_cross)
+ } else {
+ Value::new(value_cross, value_main)
+ };
+ let pos_in_gui = self.position_from_value(&value);
+
+ {
+ // Grid: subdivide each label tick in `n` grid lines:
+ let n = if step_size_in_points.abs() < 40.0 {
+ 2
+ } else if step_size_in_points.abs() < 100.0 {
+ 5
+ } else {
+ 10
+ };
+
+ for i in 0..n {
+ let strength = if i == 0 && value_main == 0.0 {
+ Strength::Strong
+ } else if i == 0 {
+ Strength::Middle
+ } else {
+ Strength::Weak
+ };
+ let color = line_color(ui, strength);
+
+ let mut pos_in_gui = pos_in_gui;
+ pos_in_gui[axis] += step_size_in_points * (i as f32) / (n as f32);
+ let mut p0 = pos_in_gui;
+ let mut p1 = pos_in_gui;
+ p0[1 - axis] = self.rect.min[1 - axis];
+ p1[1 - axis] = self.rect.max[1 - axis];
+ shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, color)));
+ }
+ }
+
+ let text = emath::round_to_decimals(value_main, 5).to_string(); // hack
+
+ let font = &ui.fonts()[text_style];
+ let galley = font.layout_multiline(text, f32::INFINITY);
+
+ let mut text_pos = pos_in_gui + vec2(1.0, -galley.size.y);
+
+ // Make sure we see the labels, even if the axis is off-screen:
+ text_pos[1 - axis] = text_pos[1 - axis]
+ .at_most(self.rect.max[1 - axis] - galley.size[1 - axis] - 2.0)
+ .at_least(self.rect.min[1 - axis] + 1.0);
+
+ shapes.push(Shape::Text {
+ pos: text_pos,
+ galley,
+ text_style,
+ color: ui.visuals().text_color(),
+ fake_italics: false,
+ });
+ }
+ }
+
+ fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) {
+ if !self.show_x && !self.show_y {
+ return;
+ }
+
+ let interact_radius: f32 = 16.0;
+ let mut closest_value = None;
+ let mut closest_curve = None;
+ let mut closest_dist_sq = interact_radius.powi(2);
+ for curve in &self.curves {
+ for value in &curve.values {
+ let pos = self.position_from_value(value);
+ let dist_sq = pointer.distance_sq(pos);
+ if dist_sq < closest_dist_sq {
+ closest_dist_sq = dist_sq;
+ closest_value = Some(value);
+ closest_curve = Some(curve);
+ }
+ }
+ }
+
+ let mut prefix = String::new();
+ if let Some(curve) = closest_curve {
+ if !curve.name.is_empty() {
+ prefix = format!("{}\n", curve.name);
+ }
+ }
+
+ let line_color = line_color(ui, Strength::Strong);
+
+ let value = if let Some(value) = closest_value {
+ let position = self.position_from_value(value);
+ shapes.push(Shape::circle_filled(position, 3.0, line_color));
+ *value
+ } else {
+ self.value_from_position(pointer)
+ };
+ let pointer = self.position_from_value(&value);
+
+ let rect = self.rect;
+
+ if self.show_x {
+ // vertical line
+ shapes.push(Shape::line_segment(
+ [pos2(pointer.x, rect.top()), pos2(pointer.x, rect.bottom())],
+ (1.0, line_color),
+ ));
+ }
+ if self.show_y {
+ // horizontal line
+ shapes.push(Shape::line_segment(
+ [pos2(rect.left(), pointer.y), pos2(rect.right(), pointer.y)],
+ (1.0, line_color),
+ ));
+ }
+
+ let text = {
+ let scale = self.dvalue_dpos();
+ let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
+ let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
+ if self.show_x && self.show_y {
+ format!(
+ "{}x = {:.*}\ny = {:.*}",
+ prefix, x_decimals, value.x, y_decimals, value.y
+ )
+ } else if self.show_x {
+ format!("{}x = {:.*}", prefix, x_decimals, value.x)
+ } else if self.show_y {
+ format!("{}y = {:.*}", prefix, y_decimals, value.y)
+ } else {
+ unreachable!()
+ }
+ };
+
+ shapes.push(Shape::text(
+ ui.fonts(),
+ pointer + vec2(3.0, -2.0),
+ Align2::LEFT_BOTTOM,
+ text,
+ TextStyle::Body,
+ ui.visuals().text_color(),
+ ));
+ }
+}
+
+#[derive(Clone, Copy)]
+enum Strength {
+ Strong,
+ Middle,
+ Weak,
+}
+
+fn line_color(ui: &Ui, strength: Strength) -> Color32 {
+ if ui.visuals().dark_mode {
+ match strength {
+ Strength::Strong => Color32::from_gray(130).additive(),
+ Strength::Middle => Color32::from_gray(55).additive(),
+ Strength::Weak => Color32::from_gray(25).additive(),
+ }
+ } else {
+ match strength {
+ Strength::Strong => Color32::from_black_alpha(220),
+ Strength::Middle => Color32::from_black_alpha(120),
+ Strength::Weak => Color32::from_black_alpha(35),
+ }
+ }
+}
diff --git a/egui_demo_lib/src/apps/demo/demo_windows.rs b/egui_demo_lib/src/apps/demo/demo_windows.rs
index fbc2045c..3b85a934 100644
--- a/egui_demo_lib/src/apps/demo/demo_windows.rs
+++ b/egui_demo_lib/src/apps/demo/demo_windows.rs
@@ -18,6 +18,7 @@ impl Default for Demos {
Box::new(super::font_book::FontBook::default()),
Box::new(super::DemoWindow::default()),
Box::new(super::painting::Painting::default()),
+ Box::new(super::plot_demo::PlotDemo::default()),
Box::new(super::scrolling::Scrolling::default()),
Box::new(super::sliders::Sliders::default()),
Box::new(super::widget_gallery::WidgetGallery::default()),
diff --git a/egui_demo_lib/src/apps/demo/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs
index b0a90a74..db42553e 100644
--- a/egui_demo_lib/src/apps/demo/mod.rs
+++ b/egui_demo_lib/src/apps/demo/mod.rs
@@ -14,6 +14,7 @@ pub mod font_contents_emoji;
pub mod font_contents_ubuntu;
pub mod layout_test;
pub mod painting;
+pub mod plot_demo;
pub mod scrolling;
pub mod sliders;
pub mod tests;
diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs
new file mode 100644
index 00000000..0b04ee0f
--- /dev/null
+++ b/egui_demo_lib/src/apps/demo/plot_demo.rs
@@ -0,0 +1,155 @@
+use egui::plot::{Curve, Plot, Value};
+use egui::*;
+use std::f64::consts::TAU;
+
+#[derive(PartialEq)]
+pub struct PlotDemo {
+ animate: bool,
+ time: f64,
+ circle_radius: f32,
+ circle_center: Pos2,
+ square: bool,
+ proportional: bool,
+}
+
+impl Default for PlotDemo {
+ fn default() -> Self {
+ Self {
+ animate: true,
+ time: 0.0,
+ circle_radius: 0.5,
+ circle_center: Pos2::new(0.0, 0.0),
+ square: false,
+ proportional: true,
+ }
+ }
+}
+
+impl super::Demo for PlotDemo {
+ fn name(&self) -> &str {
+ "🗠 Plot"
+ }
+
+ fn show(&mut self, ctx: &CtxRef, open: &mut bool) {
+ use super::View;
+ Window::new(self.name())
+ .open(open)
+ .default_size(vec2(512.0, 512.0))
+ .scroll(false)
+ .show(ctx, |ui| self.ui(ui));
+ }
+}
+
+impl PlotDemo {
+ fn options_ui(&mut self, ui: &mut Ui) {
+ ui.vertical_centered(|ui| {
+ egui::reset_button(ui, self);
+ ui.add(crate::__egui_github_link_file!());
+ });
+ ui.separator();
+
+ let Self {
+ animate,
+ time: _,
+ circle_radius,
+ circle_center,
+ square,
+ proportional,
+ } = self;
+
+ ui.horizontal(|ui| {
+ ui.group(|ui| {
+ ui.vertical(|ui| {
+ ui.label("Circle:");
+ ui.add(
+ egui::Slider::f32(circle_radius, 1e-4..=1e4)
+ .logarithmic(true)
+ .smallest_positive(1e-2)
+ .text("radius"),
+ );
+ ui.add(
+ egui::Slider::f32(&mut circle_center.x, -1e4..=1e4)
+ .logarithmic(true)
+ .smallest_positive(1e-2)
+ .text("center x"),
+ );
+ ui.add(
+ egui::Slider::f32(&mut circle_center.y, -1e4..=1e4)
+ .logarithmic(true)
+ .smallest_positive(1e-2)
+ .text("center y"),
+ );
+ });
+ });
+
+ ui.vertical(|ui| {
+ ui.style_mut().wrap = Some(false);
+ ui.checkbox(animate, "animate");
+ ui.advance_cursor(8.0);
+ ui.checkbox(square, "square view");
+ ui.checkbox(proportional, "proportional data axes");
+ });
+ });
+ }
+
+ fn circle(&self) -> Curve {
+ let n = 512;
+ let circle = (0..=n).map(|i| {
+ let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU);
+ let r = self.circle_radius as f64;
+ Value::new(
+ r * t.cos() + self.circle_center.x as f64,
+ r * t.sin() + self.circle_center.y as f64,
+ )
+ });
+ Curve::from_values_iter(circle)
+ .color(Color32::from_rgb(100, 200, 100))
+ .name("circle")
+ }
+
+ fn sin(&self) -> Curve {
+ let n = 512;
+ let circle = (0..=n).map(|i| {
+ let t = remap(i as f64, 0.0..=(n as f64), -TAU..=TAU);
+ Value::new(t / 5.0, 0.5 * (self.time + t).sin())
+ });
+ Curve::from_values_iter(circle)
+ .color(Color32::from_rgb(200, 100, 100))
+ .name("0.5 * sin(x / 5)")
+ }
+
+ fn thingy(&self) -> Curve {
+ let n = 512;
+ let complex_curve = (0..=n).map(|i| {
+ let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU);
+ Value::new((2.0 * t + self.time).sin(), (3.0 * t).sin())
+ });
+ Curve::from_values_iter(complex_curve)
+ .color(Color32::from_rgb(100, 150, 250))
+ .name("x = sin(2t), y = sin(3t)")
+ }
+}
+
+impl super::View for PlotDemo {
+ fn ui(&mut self, ui: &mut Ui) {
+ self.options_ui(ui);
+
+ if self.animate {
+ ui.ctx().request_repaint();
+ self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64;
+ };
+
+ let mut plot = Plot::default()
+ .curve(self.circle())
+ .curve(self.sin())
+ .curve(self.thingy())
+ .min_size(Vec2::new(400.0, 300.0));
+ if self.square {
+ plot = plot.view_aspect(1.0);
+ }
+ if self.proportional {
+ plot = plot.data_aspect(1.0);
+ }
+ ui.add(plot);
+ }
+}
diff --git a/emath/src/pos2.rs b/emath/src/pos2.rs
index ba9d47aa..7afcb3aa 100644
--- a/emath/src/pos2.rs
+++ b/emath/src/pos2.rs
@@ -8,7 +8,7 @@ use crate::*;
///
/// Mathematically this is known as a "point", but the term position was chosen so not to
/// conflict with the unit (one point = X physical pixels).
-#[derive(Clone, Copy, Default)]
+#[derive(Clone, Copy, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Pos2 {
pub x: f32,
@@ -145,11 +145,27 @@ impl Pos2 {
}
}
-impl PartialEq for Pos2 {
- fn eq(&self, other: &Self) -> bool {
- self.x == other.x && self.y == other.y
+impl std::ops::Index for Pos2 {
+ type Output = f32;
+ fn index(&self, index: usize) -> &f32 {
+ match index {
+ 0 => &self.x,
+ 1 => &self.y,
+ _ => panic!("Pos2 index out of bounds: {}", index),
+ }
}
}
+
+impl std::ops::IndexMut for Pos2 {
+ fn index_mut(&mut self, index: usize) -> &mut f32 {
+ match index {
+ 0 => &mut self.x,
+ 1 => &mut self.y,
+ _ => panic!("Pos2 index out of bounds: {}", index),
+ }
+ }
+}
+
impl Eq for Pos2 {}
impl AddAssign for Pos2 {
diff --git a/emath/src/rect.rs b/emath/src/rect.rs
index fe4fc6f4..a7a3caac 100644
--- a/emath/src/rect.rs
+++ b/emath/src/rect.rs
@@ -180,6 +180,18 @@ impl Rect {
self.max = self.max.max(p);
}
+ /// Expand to include the given x coordinate
+ pub fn extend_with_x(&mut self, x: f32) {
+ self.min.x = self.min.x.min(x);
+ self.max.x = self.max.x.max(x);
+ }
+
+ /// Expand to include the given y coordinate
+ pub fn extend_with_y(&mut self, y: f32) {
+ self.min.y = self.min.y.min(y);
+ self.max.y = self.max.y.max(y);
+ }
+
pub fn union(self, other: Rect) -> Rect {
Rect {
min: self.min.min(other.min),
diff --git a/emath/src/rect_transform.rs b/emath/src/rect_transform.rs
index e758652c..28292def 100644
--- a/emath/src/rect_transform.rs
+++ b/emath/src/rect_transform.rs
@@ -26,6 +26,11 @@ impl RectTransform {
&self.to
}
+ /// The scale factors.
+ pub fn scale(&self) -> Vec2 {
+ self.to.size() / self.from.size()
+ }
+
pub fn inverse(&self) -> RectTransform {
Self::from_to(self.to, self.from)
}
diff --git a/emath/src/vec2.rs b/emath/src/vec2.rs
index 65309381..4a59904c 100644
--- a/emath/src/vec2.rs
+++ b/emath/src/vec2.rs
@@ -8,7 +8,7 @@ use crate::*;
/// emath represents positions using [`Pos2`].
///
/// Normally the units are points (logical pixels).
-#[derive(Clone, Copy, Default)]
+#[derive(Clone, Copy, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Vec2 {
pub x: f32,
@@ -186,11 +186,27 @@ impl Vec2 {
}
}
-impl PartialEq for Vec2 {
- fn eq(&self, other: &Self) -> bool {
- self.x == other.x && self.y == other.y
+impl std::ops::Index for Vec2 {
+ type Output = f32;
+ fn index(&self, index: usize) -> &f32 {
+ match index {
+ 0 => &self.x,
+ 1 => &self.y,
+ _ => panic!("Vec2 index out of bounds: {}", index),
+ }
}
}
+
+impl std::ops::IndexMut for Vec2 {
+ fn index_mut(&mut self, index: usize) -> &mut f32 {
+ match index {
+ 0 => &mut self.x,
+ 1 => &mut self.y,
+ _ => panic!("Vec2 index out of bounds: {}", index),
+ }
+ }
+}
+
impl Eq for Vec2 {}
impl Neg for Vec2 {
@@ -239,6 +255,28 @@ impl Sub for Vec2 {
}
}
+/// Element-wise multiplication
+impl Mul for Vec2 {
+ type Output = Vec2;
+ fn mul(self, vec: Vec2) -> Vec2 {
+ Vec2 {
+ x: self.x * vec.x,
+ y: self.y * vec.y,
+ }
+ }
+}
+
+/// Element-wise division
+impl Div for Vec2 {
+ type Output = Vec2;
+ fn div(self, rhs: Vec2) -> Vec2 {
+ Vec2 {
+ x: self.x / rhs.x,
+ y: self.y / rhs.y,
+ }
+ }
+}
+
impl MulAssign for Vec2 {
fn mul_assign(&mut self, rhs: f32) {
self.x *= rhs;
diff --git a/epaint/src/color.rs b/epaint/src/color.rs
index 963a4ca2..7332a410 100644
--- a/epaint/src/color.rs
+++ b/epaint/src/color.rs
@@ -124,6 +124,12 @@ impl Color32 {
Rgba::from(self).to_opaque().into()
}
+ /// Returns an additive version of self
+ pub fn additive(self) -> Self {
+ let [r, g, b, _] = self.to_array();
+ Self([r, g, b, 0])
+ }
+
/// Premultiplied RGBA
pub fn to_array(&self) -> [u8; 4] {
[self.r(), self.g(), self.b(), self.a()]