diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index fd8e249b..448349e1 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -305,7 +305,7 @@ impl CollapsingHeader { .unwrap_or_else(|| visuals.text_color()); if ui.visuals().collapsing_header_frame || self.show_background { - ui.painter().add(Shape::Rect { + ui.painter().add(epaint::RectShape { rect: header_response.rect.expand(visuals.expansion), corner_radius: visuals.corner_radius, fill: visuals.bg_fill, diff --git a/egui/src/containers/combo_box.rs b/egui/src/containers/combo_box.rs index 5d171acc..e2761a08 100644 --- a/egui/src/containers/combo_box.rs +++ b/egui/src/containers/combo_box.rs @@ -238,7 +238,7 @@ fn button_frame( ui.painter().set( where_to_put_background, - Shape::Rect { + epaint::RectShape { rect: outer_rect.expand(visuals.expansion), corner_radius: visuals.corner_radius, fill: visuals.bg_fill, diff --git a/egui/src/containers/frame.rs b/egui/src/containers/frame.rs index c95ebe4f..29c1d7ce 100644 --- a/egui/src/containers/frame.rs +++ b/egui/src/containers/frame.rs @@ -178,12 +178,12 @@ impl Frame { stroke, } = *self; - let frame_shape = Shape::Rect { + let frame_shape = Shape::Rect(epaint::RectShape { rect: outer_rect, corner_radius, fill, stroke, - }; + }); if shadow == Default::default() { frame_shape diff --git a/egui/src/containers/resize.rs b/egui/src/containers/resize.rs index 2450ad27..92d9436c 100644 --- a/egui/src/containers/resize.rs +++ b/egui/src/containers/resize.rs @@ -282,12 +282,11 @@ impl Resize { if self.with_stroke && corner_response.is_some() { let rect = Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size); let rect = rect.expand(2.0); // breathing room for content - ui.painter().add(epaint::Shape::Rect { + ui.painter().add(Shape::rect_stroke( rect, - corner_radius: 3.0, - fill: Default::default(), - stroke: ui.visuals().widgets.noninteractive.bg_stroke, - }); + 3.0, + ui.visuals().widgets.noninteractive.bg_stroke, + )); } if let Some(corner_response) = corner_response { diff --git a/egui/src/context.rs b/egui/src/context.rs index 1a9e9e8e..dc0e21b4 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -569,7 +569,7 @@ impl Context { }; if self.fonts.is_none() || new_font_definitions.is_some() || pixels_per_point_changed { - self.fonts = Some(Arc::new(Fonts::from_definitions( + self.fonts = Some(Arc::new(Fonts::new( pixels_per_point, new_font_definitions.unwrap_or_else(|| { self.fonts diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index 63415a51..a91cc3f9 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -10,7 +10,8 @@ use crate::emath::*; /// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`]. /// /// All coordinates are in points (logical pixels) with origin (0, 0) in the top left corner. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct RawInput { /// How many points (logical pixels) the user scrolled pub scroll_delta: Vec2, @@ -100,10 +101,38 @@ impl RawInput { dropped_files: std::mem::take(&mut self.dropped_files), } } + + /// Add on new input. + pub fn append(&mut self, newer: Self) { + let Self { + scroll_delta, + zoom_delta, + screen_rect, + pixels_per_point, + time, + predicted_dt, + modifiers, + mut events, + mut hovered_files, + mut dropped_files, + } = newer; + + self.scroll_delta += scroll_delta; + self.zoom_delta *= zoom_delta; + self.screen_rect = screen_rect.or(self.screen_rect); + self.pixels_per_point = pixels_per_point.or(self.pixels_per_point); + self.time = time; // use latest time + self.predicted_dt = predicted_dt; // use latest dt + self.modifiers = modifiers; // use latest + self.events.append(&mut events); + self.hovered_files.append(&mut hovered_files); + self.dropped_files.append(&mut dropped_files); + } } /// A file about to be dropped into egui. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct HoveredFile { /// Set by the `egui_glium` backend. pub path: Option, @@ -112,7 +141,8 @@ pub struct HoveredFile { } /// A file dropped into egui. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct DroppedFile { /// Set by the `egui_glium` backend. pub path: Option, @@ -128,6 +158,7 @@ pub struct DroppedFile { /// /// This only covers events that egui cares about. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub enum Event { /// The integration detected a "copy" event (e.g. Cmd+C). Copy, @@ -186,6 +217,7 @@ pub enum Event { /// Mouse button (or similar for touch input) #[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub enum PointerButton { /// The primary mouse button is usually the left one. Primary = 0, @@ -201,6 +233,7 @@ pub const NUM_POINTER_BUTTONS: usize = 3; /// State of the modifier keys. These must be fed to egui. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct Modifiers { /// Either of the alt keys are down (option ⌥ on Mac). pub alt: bool, @@ -350,16 +383,19 @@ impl RawInput { /// this is a `u64` as values of this kind can always be obtained by hashing #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct TouchDeviceId(pub u64); /// Unique identification of a touch occurrence (finger or pen or ...). /// A Touch ID is valid until the finger is lifted. /// A new ID is used for the next touch. #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct TouchId(pub u64); /// In what phase a touch event is in. #[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub enum TouchPhase { /// User just placed a touch point on the touch surface Start, diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index ff145b52..e3b1d0ec 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -5,6 +5,7 @@ use crate::WidgetType; /// What egui emits each frame. /// The backend should use this. #[derive(Clone, Default, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct Output { /// Set the cursor to this icon. pub cursor_icon: CursorIcon, @@ -12,7 +13,9 @@ pub struct Output { /// If set, open this url. pub open_url: Option, - /// Response to [`crate::Event::Copy`] or [`crate::Event::Cut`]. Ignore if empty. + /// If set, put this text in the system clipboard. Ignore if empty. + /// + /// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`]. pub copied_text: String, /// If `true`, egui is requesting immediate repaint (i.e. on the next frame). @@ -53,9 +56,40 @@ impl Output { } Default::default() } + + /// Add on new output. + pub fn append(&mut self, newer: Self) { + let Self { + cursor_icon, + open_url, + copied_text, + needs_repaint, + mut events, + text_cursor_pos, + } = newer; + + self.cursor_icon = cursor_icon; + if open_url.is_some() { + self.open_url = open_url; + } + if !copied_text.is_empty() { + self.copied_text = copied_text; + } + self.needs_repaint = needs_repaint; // if the last frame doesn't need a repaint, then we don't need to repaint + self.events.append(&mut events); + self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos); + } + + /// Take everything ephemeral (everything except `cursor_icon` currently) + pub fn take(&mut self) -> Self { + let taken = std::mem::take(self); + self.cursor_icon = taken.cursor_icon; // eveything else is ephemeral + taken + } } #[derive(Clone, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct OpenUrl { pub url: String, /// If `true`, open the url in a new tab. @@ -88,6 +122,7 @@ impl OpenUrl { /// /// Loosely based on . #[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub enum CursorIcon { /// Normal cursor icon, whatever that is. Default, @@ -209,6 +244,7 @@ impl Default for CursorIcon { /// /// In particular, these events may be useful for accessability, i.e. for screen readers. #[derive(Clone, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub enum OutputEvent { // A widget was clicked. Clicked(WidgetInfo), @@ -236,6 +272,7 @@ impl std::fmt::Debug for OutputEvent { /// Describes a widget such as a [`crate::Button`] or a [`crate::TextEdit`]. #[derive(Clone, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct WidgetInfo { /// The type of widget this is. pub typ: WidgetType, diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 8f0ba00f..b842b833 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -378,7 +378,7 @@ impl Default for PointerState { interact_pos: None, delta: Vec2::ZERO, velocity: Vec2::ZERO, - pos_history: History::new(1000, 0.1), + pos_history: History::new(0..1000, 0.1), down: Default::default(), press_origin: None, press_start_time: None, diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 1aed0565..5f06f183 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -89,6 +89,8 @@ impl Widget for &epaint::stats::PaintStats { shape_path, shape_mesh, shape_vec, + text_shape_vertices, + text_shape_indices, clipped_meshes, vertices, indices, @@ -96,13 +98,19 @@ impl Widget for &epaint::stats::PaintStats { ui.label("Intermediate:"); label(ui, shapes, "shapes").on_hover_text("Boxes, circles, etc"); - label(ui, shape_text, "text"); + label(ui, shape_text, "text (mostly cached)"); label(ui, shape_path, "paths"); label(ui, shape_mesh, "nested meshes"); label(ui, shape_vec, "nested shapes"); ui.add_space(10.0); - ui.label("Tessellated:"); + ui.label("Text shapes:"); + label(ui, text_shape_vertices, "vertices"); + label(ui, text_shape_indices, "indices") + .on_hover_text("Three 32-bit indices per triangles"); + ui.add_space(10.0); + + ui.label("Tessellated (and culled):"); label(ui, clipped_meshes, "clipped_meshes") .on_hover_text("Number of separate clip rectangles"); label(ui, vertices, "vertices"); diff --git a/egui/src/lib.rs b/egui/src/lib.rs index 9817baad..7d05f0e0 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -397,7 +397,7 @@ pub use { }; pub mod text { - pub use epaint::text::{Galley, LayoutJob, LayoutSection, TextFormat, TAB_SIZE}; + pub use epaint::text::{Fonts, Galley, LayoutJob, LayoutSection, TextFormat, TAB_SIZE}; } // ---------------------------------------------------------------------------- @@ -527,6 +527,7 @@ pub mod special_emojis { /// The different types of built-in widgets in egui #[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub enum WidgetType { Label, // TODO: emit Label events Hyperlink, diff --git a/egui/src/painter.rs b/egui/src/painter.rs index c500fef2..4426f222 100644 --- a/egui/src/painter.rs +++ b/egui/src/painter.rs @@ -6,7 +6,7 @@ use crate::{ use epaint::{ mutex::Mutex, text::{Fonts, Galley, TextStyle}, - Shape, Stroke, TextShape, + CircleShape, RectShape, Shape, Stroke, TextShape, }; /// Helper to paint shapes and text to a specific region on a specific layer. @@ -183,10 +183,11 @@ impl Painter { } /// Modify an existing [`Shape`]. - pub fn set(&self, idx: ShapeIdx, mut shape: Shape) { + pub fn set(&self, idx: ShapeIdx, shape: impl Into) { if self.fade_to_color == Some(Color32::TRANSPARENT) { return; } + let mut shape = shape.into(); self.transform_shape(&mut shape); self.paint_list.lock().set(idx, self.clip_rect, shape) } @@ -223,13 +224,11 @@ impl Painter { let galley = self.layout_no_wrap(text.to_string(), TextStyle::Monospace, color); let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size())); let frame_rect = rect.expand(2.0); - self.add(Shape::Rect { - rect: frame_rect, - corner_radius: 0.0, - fill: Color32::from_black_alpha(240), - // stroke: Stroke::new(1.0, color), - stroke: Default::default(), - }); + self.add(Shape::rect_filled( + frame_rect, + 0.0, + Color32::from_black_alpha(240), + )); self.galley(rect.min, galley); frame_rect } @@ -251,7 +250,7 @@ impl Painter { fill_color: impl Into, stroke: impl Into, ) { - self.add(Shape::Circle { + self.add(CircleShape { center, radius, fill: fill_color.into(), @@ -260,7 +259,7 @@ impl Painter { } pub fn circle_filled(&self, center: Pos2, radius: f32, fill_color: impl Into) { - self.add(Shape::Circle { + self.add(CircleShape { center, radius, fill: fill_color.into(), @@ -269,7 +268,7 @@ impl Painter { } pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into) { - self.add(Shape::Circle { + self.add(CircleShape { center, radius, fill: Default::default(), @@ -284,7 +283,7 @@ impl Painter { fill_color: impl Into, stroke: impl Into, ) { - self.add(Shape::Rect { + self.add(RectShape { rect, corner_radius, fill: fill_color.into(), @@ -293,7 +292,7 @@ impl Painter { } pub fn rect_filled(&self, rect: Rect, corner_radius: f32, fill_color: impl Into) { - self.add(Shape::Rect { + self.add(RectShape { rect, corner_radius, fill: fill_color.into(), @@ -302,7 +301,7 @@ impl Painter { } pub fn rect_stroke(&self, rect: Rect, corner_radius: f32, stroke: impl Into) { - self.add(Shape::Rect { + self.add(RectShape { rect, corner_radius, fill: Default::default(), diff --git a/egui/src/util/history.rs b/egui/src/util/history.rs index ea91392d..f16592ad 100644 --- a/egui/src/util/history.rs +++ b/egui/src/util/history.rs @@ -2,11 +2,13 @@ use std::collections::VecDeque; /// This struct tracks recent values of some time series. /// -/// One use is to show a log of recent events, -/// or show a graph over recent events. +/// It can be used as a smoothing filter for e.g. latency, fps etc, +/// or to show a log or graph of recent events. /// -/// It has both a maximum length and a maximum storage time. -/// Elements are dropped when either max length or max age is reached. +/// It has a minimum and maximum length, as well as a maximum storage time. +/// * The minimum length is to ensure you have enough data for an estimate. +/// * The maximum length is to make sure the history doesn't take up too much space. +/// * The maximum age is to make sure the estimate isn't outdated. /// /// Time difference between values can be zero, but never negative. /// @@ -15,11 +17,15 @@ use std::collections::VecDeque; /// All times are in seconds. #[derive(Clone, Debug)] pub struct History { - /// In elements, i.e. of `values.len()` + /// In elements, i.e. of `values.len()`. + /// The length is initially zero, but once past `min_len` will not shrink below it. + min_len: usize, + + /// In elements, i.e. of `values.len()`. max_len: usize, - /// In seconds - max_age: f64, // TODO: f32 + /// In seconds. + max_age: f32, /// Total number of elements seen ever total_count: u64, @@ -33,13 +39,22 @@ impl History where T: Copy, { - pub fn new(max_len: usize, max_age: f64) -> Self { - Self::from_max_len_age(max_len, max_age) - } - - pub fn from_max_len_age(max_len: usize, max_age: f64) -> Self { + /// Example: + /// ``` + /// # use egui::util::History; + /// # fn now() -> f64 { 0.0 } + /// // Drop events that are older than one second, + /// // as long we keep at least two events. Never keep more than a hundred events. + /// let mut history = History::new(2..100, 1.0); + /// assert_eq!(history.average(), None); + /// history.add(now(), 40.0_f32); + /// history.add(now(), 44.0_f32); + /// assert_eq!(history.average(), Some(42.0)); + /// ``` + pub fn new(length_range: std::ops::Range, max_age: f32) -> Self { Self { - max_len, + min_len: length_range.start, + max_len: length_range.end, max_age, total_count: 0, values: Default::default(), @@ -51,7 +66,7 @@ where } pub fn max_age(&self) -> f32 { - self.max_age as f32 + self.max_age } pub fn is_empty(&self) -> bool { @@ -125,14 +140,23 @@ where } } + // Mean number of events per second. + pub fn rate(&self) -> Option { + self.mean_time_interval().map(|time| 1.0 / time) + } + /// Remove samples that are too old pub fn flush(&mut self, now: f64) { while self.values.len() > self.max_len { self.values.pop_front(); } - while let Some((front_time, _)) = self.values.front() { - if *front_time < now - self.max_age { - self.values.pop_front(); + while self.values.len() > self.min_len { + if let Some((front_time, _)) = self.values.front() { + if *front_time < now - (self.max_age as f64) { + self.values.pop_front(); + } else { + break; + } } else { break; } @@ -160,6 +184,21 @@ where } } +impl History +where + T: Copy, + T: std::iter::Sum, + T: std::ops::Div, + T: std::ops::Mul, +{ + /// Average times rate. + /// If you are keeping track of individual sizes of things (e.g. bytes), + /// this will estimate the bandwidth (bytes per second). + pub fn bandwidth(&self) -> Option { + Some(self.average()? * self.rate()?) + } +} + impl History where T: Copy, diff --git a/egui/src/widgets/button.rs b/egui/src/widgets/button.rs index 3d173b9e..4b89915e 100644 --- a/egui/src/widgets/button.rs +++ b/egui/src/widgets/button.rs @@ -309,7 +309,7 @@ impl<'a> Widget for Checkbox<'a> { rect.center().y - 0.5 * galley.size().y, ); let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); - ui.painter().add(Shape::Rect { + ui.painter().add(epaint::RectShape { rect: big_icon_rect.expand(visuals.expansion), corner_radius: visuals.corner_radius, fill: visuals.bg_fill, @@ -433,7 +433,7 @@ impl Widget for RadioButton { let painter = ui.painter(); - painter.add(Shape::Circle { + painter.add(epaint::CircleShape { center: big_icon_rect.center(), radius: big_icon_rect.width() / 2.0 + visuals.expansion, fill: visuals.bg_fill, @@ -441,7 +441,7 @@ impl Widget for RadioButton { }); if checked { - painter.add(Shape::Circle { + painter.add(epaint::CircleShape { center: small_icon_rect.center(), radius: small_icon_rect.width() / 3.0, fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill diff --git a/egui/src/widgets/color_picker.rs b/egui/src/widgets/color_picker.rs index 3cd2456c..3e1d24ad 100644 --- a/egui/src/widgets/color_picker.rs +++ b/egui/src/widgets/color_picker.rs @@ -57,7 +57,7 @@ fn show_hsva(ui: &mut Ui, color: Hsva, desired_size: Vec2) -> Response { ui.painter().rect_filled(left, 0.0, color); ui.painter().rect_filled(right, 0.0, color.to_opaque()); } else { - ui.painter().add(Shape::Rect { + ui.painter().add(RectShape { rect, corner_radius: 2.0, fill: color.into(), @@ -190,7 +190,7 @@ fn color_slider_2d( let x = lerp(rect.left()..=rect.right(), *x_value); let y = lerp(rect.bottom()..=rect.top(), *y_value); let picked_color = color_at(*x_value, *y_value); - ui.painter().add(Shape::Circle { + ui.painter().add(epaint::CircleShape { center: pos2(x, y), radius: rect.width() / 12.0, fill: picked_color, diff --git a/egui/src/widgets/plot/items.rs b/egui/src/widgets/plot/items.rs index f7af7ca0..48268744 100644 --- a/egui/src/widgets/plot/items.rs +++ b/egui/src/widgets/plot/items.rs @@ -800,12 +800,7 @@ impl PlotItem for Polygon { let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha); - let shape = Shape::Path { - points: values_tf.clone(), - closed: true, - fill: fill.into(), - stroke: Stroke::none(), - }; + let shape = Shape::convex_polygon(values_tf.clone(), fill, Stroke::none()); shapes.push(shape); values_tf.push(*values_tf.first().unwrap()); style.style_line(values_tf, *stroke, *highlight, shapes); @@ -1081,21 +1076,16 @@ impl PlotItem for Points { match shape { MarkerShape::Circle => { - shapes.push(Shape::Circle { + shapes.push(Shape::Circle(epaint::CircleShape { center, radius, fill, stroke, - }); + })); } MarkerShape::Diamond => { let points = vec![tf(1.0, 0.0), tf(0.0, -1.0), tf(-1.0, 0.0), tf(0.0, 1.0)]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); + shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Square => { let points = vec![ @@ -1104,12 +1094,7 @@ impl PlotItem for Points { tf(-frac_1_sqrt_2, -frac_1_sqrt_2), tf(-frac_1_sqrt_2, frac_1_sqrt_2), ]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); + shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Cross => { let diagonal1 = [ @@ -1132,12 +1117,7 @@ impl PlotItem for Points { MarkerShape::Up => { let points = vec![tf(0.0, -1.0), tf(-0.5 * sqrt_3, 0.5), tf(0.5 * sqrt_3, 0.5)]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); + shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Down => { let points = vec![ @@ -1145,22 +1125,12 @@ impl PlotItem for Points { tf(-0.5 * sqrt_3, -0.5), tf(0.5 * sqrt_3, -0.5), ]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); + shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Left => { let points = vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); + shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Right => { let points = vec![ @@ -1168,12 +1138,7 @@ impl PlotItem for Points { tf(-0.5, -0.5 * sqrt_3), tf(-0.5, 0.5 * sqrt_3), ]; - shapes.push(Shape::Path { - points, - closed: true, - fill, - stroke, - }); + shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Asterisk => { let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 239dd31f..f461f622 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -117,7 +117,7 @@ impl LegendEntry { let painter = ui.painter(); - painter.add(Shape::Circle { + painter.add(epaint::CircleShape { center: icon_rect.center(), radius: icon_size * 0.5, fill: visuals.bg_fill, @@ -130,12 +130,11 @@ impl LegendEntry { } else { *color }; - painter.add(Shape::Circle { - center: icon_rect.center(), - radius: icon_size * 0.4, + painter.add(epaint::Shape::circle_filled( + icon_rect.center(), + icon_size * 0.4, fill, - stroke: Default::default(), - }); + )); } let text_position_x = if label_on_the_left { diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 637119d7..60bc25d2 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -404,7 +404,7 @@ impl Widget for Plot { // Background if show_background { - plot_painter.add(Shape::Rect { + plot_painter.add(epaint::RectShape { rect, corner_radius: 2.0, fill: ui.visuals().extreme_bg_color, diff --git a/egui/src/widgets/progress_bar.rs b/egui/src/widgets/progress_bar.rs index 50ae160d..dec6dfdf 100644 --- a/egui/src/widgets/progress_bar.rs +++ b/egui/src/widgets/progress_bar.rs @@ -116,12 +116,10 @@ impl Widget for ProgressBar { + vec2(-corner_radius, 0.0) }) .collect(); - ui.painter().add(Shape::Path { + ui.painter().add(Shape::line( points, - closed: false, - fill: Color32::TRANSPARENT, - stroke: Stroke::new(2.0, visuals.faint_bg_color), - }); + Stroke::new(2.0, visuals.faint_bg_color), + )); } if let Some(text_kind) = text { diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 34fc206b..cd84682d 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -333,7 +333,7 @@ impl<'a> Slider<'a> { let marker_center_x = self.x_from_value(value, x_range); let visuals = ui.style().interact(response); - ui.painter().add(Shape::Rect { + ui.painter().add(epaint::RectShape { rect: rail_rect, corner_radius: ui.visuals().widgets.inactive.corner_radius, fill: ui.visuals().widgets.inactive.bg_fill, @@ -344,7 +344,7 @@ impl<'a> Slider<'a> { // stroke: ui.visuals().widgets.inactive.bg_stroke, }); - ui.painter().add(Shape::Circle { + ui.painter().add(epaint::CircleShape { center: pos2(marker_center_x, rail_rect.center().y), radius: handle_radius(rect) + visuals.expansion, fill: visuals.bg_fill, diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index c1ee1f9f..28d37194 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -425,7 +425,7 @@ impl<'t, S: TextBuffer> Widget for TextEdit<'t, S> { let visuals = ui.style().interact(&response); let frame_rect = response.rect.expand(visuals.expansion); let shape = if response.has_focus() { - Shape::Rect { + epaint::RectShape { rect: frame_rect, corner_radius: visuals.corner_radius, // fill: ui.visuals().selection.bg_fill, @@ -433,7 +433,7 @@ impl<'t, S: TextBuffer> Widget for TextEdit<'t, S> { stroke: ui.visuals().selection.stroke, } } else { - Shape::Rect { + epaint::RectShape { rect: frame_rect, corner_radius: visuals.corner_radius, fill: ui.visuals().extreme_bg_color, diff --git a/egui_demo_lib/benches/benchmark.rs b/egui_demo_lib/benches/benchmark.rs index c44210d6..70e4ad31 100644 --- a/egui_demo_lib/benches/benchmark.rs +++ b/egui_demo_lib/benches/benchmark.rs @@ -70,10 +70,8 @@ pub fn criterion_benchmark(c: &mut Criterion) { let wrap_width = 512.0; let text_style = egui::TextStyle::Body; let color = egui::Color32::WHITE; - let fonts = egui::epaint::text::Fonts::from_definitions( - pixels_per_point, - egui::FontDefinitions::default(), - ); + let fonts = + egui::epaint::text::Fonts::new(pixels_per_point, egui::FontDefinitions::default()); c.bench_function("text_layout_uncached", |b| { b.iter(|| { use egui::epaint::text::{layout, LayoutJob}; diff --git a/egui_demo_lib/src/apps/demo/drag_and_drop.rs b/egui_demo_lib/src/apps/demo/drag_and_drop.rs index f881384f..c5d4db64 100644 --- a/egui_demo_lib/src/apps/demo/drag_and_drop.rs +++ b/egui_demo_lib/src/apps/demo/drag_and_drop.rs @@ -65,7 +65,7 @@ pub fn drop_target( ui.painter().set( where_to_put_background, - Shape::Rect { + epaint::RectShape { corner_radius: style.corner_radius, fill, stroke, diff --git a/egui_demo_lib/src/frame_history.rs b/egui_demo_lib/src/frame_history.rs index bebe2e16..1a565d7b 100644 --- a/egui_demo_lib/src/frame_history.rs +++ b/egui_demo_lib/src/frame_history.rs @@ -6,9 +6,10 @@ pub struct FrameHistory { impl Default for FrameHistory { fn default() -> Self { - let max_age: f64 = 1.0; + let max_age: f32 = 1.0; + let max_len = (max_age * 300.0).round() as usize; Self { - frame_times: History::from_max_len_age((max_age * 300.0).round() as usize, max_age), + frame_times: History::new(0..max_len, max_age), } } } @@ -73,12 +74,12 @@ impl FrameHistory { let to_screen = emath::RectTransform::from_to(graph_rect, rect); let mut shapes = Vec::with_capacity(3 + 2 * history.len()); - shapes.push(Shape::Rect { + shapes.push(Shape::Rect(epaint::RectShape { rect, corner_radius: style.corner_radius, fill: ui.visuals().extreme_bg_color, stroke: ui.style().noninteractive().bg_stroke, - }); + })); let rect = rect.shrink(4.0); let color = ui.visuals().text_color(); diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 84151d7c..bed15c32 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -34,7 +34,6 @@ use { }; pub use copypasta::ClipboardContext; -use std::borrow::BorrowMut; // TODO: remove pub struct GliumInputState { pub pointer_pos_in_points: Option, @@ -556,10 +555,21 @@ impl EguiGlium { &self.egui_ctx } + pub fn painter_mut(&mut self) -> &mut crate::Painter { + &mut self.painter + } + pub fn ctx_and_painter_mut(&mut self) -> (&egui::CtxRef, &mut crate::Painter) { (&self.egui_ctx, &mut self.painter) } + pub fn pixels_per_point(&self) -> f32 { + self.input_state + .raw + .pixels_per_point + .unwrap_or_else(|| self.egui_ctx.pixels_per_point()) + } + pub fn on_event(&mut self, event: &glium::glutin::event::WindowEvent<'_>) { crate::input_to_egui( self.egui_ctx.pixels_per_point(), @@ -575,13 +585,19 @@ impl EguiGlium { } pub fn begin_frame(&mut self, display: &glium::Display) { - let pixels_per_point = self - .input_state - .raw - .pixels_per_point - .unwrap_or_else(|| self.egui_ctx.pixels_per_point()); + let raw_input = self.take_raw_input(display); + self.begin_frame_with_input(raw_input); + } - self.input_state.raw.time = Some(self.start_time.elapsed().as_nanos() as f64 * 1e-9); + pub fn begin_frame_with_input(&mut self, raw_input: RawInput) { + self.egui_ctx.begin_frame(raw_input); + } + + /// Prepare for a new frame. Normally you would call [`Self::begin_frame`] instead. + pub fn take_raw_input(&mut self, display: &glium::Display) -> egui::RawInput { + let pixels_per_point = self.pixels_per_point(); + + self.input_state.raw.time = Some(self.start_time.elapsed().as_secs_f64()); // On Windows, a minimized window will have 0 width and height. // See: https://github.com/rust-windowing/winit/issues/208 @@ -596,7 +612,7 @@ impl EguiGlium { None }; - self.egui_ctx.begin_frame(self.input_state.raw.take()); + self.input_state.raw.take() } /// Returns `needs_repaint` and shapes to draw. @@ -605,10 +621,16 @@ impl EguiGlium { display: &glium::Display, ) -> (bool, Vec) { let (egui_output, shapes) = self.egui_ctx.end_frame(); + let needs_repaint = egui_output.needs_repaint; + self.handle_output(display, egui_output); + (needs_repaint, shapes) + } + pub fn handle_output(&mut self, display: &glium::Display, egui_output: egui::Output) { if self.egui_ctx.memory().options.screen_reader { self.screen_reader.speak(&egui_output.events_description()); } + if self.current_cursor_icon != egui_output.cursor_icon { // call only when changed to prevent flickering near frame boundary // when Windows OS tries to control cursor icon for window resizing @@ -616,11 +638,7 @@ impl EguiGlium { self.current_cursor_icon = egui_output.cursor_icon; } - let needs_repaint = egui_output.needs_repaint; - handle_output(egui_output, self.clipboard.as_mut(), display); - - (needs_repaint, shapes) } pub fn paint( @@ -638,8 +656,4 @@ impl EguiGlium { &self.egui_ctx.texture(), ); } - - pub fn painter_mut(&mut self) -> &mut Painter { - self.painter.borrow_mut() - } } diff --git a/emath/src/rect.rs b/emath/src/rect.rs index 446ddf34..5cf7520c 100644 --- a/emath/src/rect.rs +++ b/emath/src/rect.rs @@ -88,6 +88,15 @@ impl Rect { } } + /// Bounding-box around the points + pub fn from_points(points: &[Pos2]) -> Self { + let mut rect = Rect::NOTHING; + for &p in points { + rect.extend_with(p); + } + rect + } + /// A `Rect` that contains every point to the right of the given X coordinate. #[inline] pub fn everything_right_of(left_x: f32) -> Self { @@ -165,15 +174,6 @@ impl Rect { ) } - /// The intersection of two `Rect`, i.e. the area covered by both. - #[must_use] - pub fn intersect(self, other: Rect) -> Self { - Self { - min: self.min.max(other.min), - max: self.max.min(other.max), - } - } - #[must_use] #[inline] pub fn intersects(self, other: Rect) -> bool { @@ -236,7 +236,10 @@ impl Rect { self.max.y = self.max.y.max(y); } + /// The union of two bounding rectangle, i.e. the minimum `Rect` + /// that contains both input rectangles. #[inline(always)] + #[must_use] pub fn union(self, other: Rect) -> Rect { Rect { min: self.min.min(other.min), @@ -244,6 +247,16 @@ impl Rect { } } + /// The intersection of two `Rect`, i.e. the area covered by both. + #[inline] + #[must_use] + pub fn intersect(self, other: Rect) -> Self { + Self { + min: self.min.max(other.min), + max: self.max.min(other.max), + } + } + #[inline(always)] pub fn center(&self) -> Pos2 { Pos2 { diff --git a/epaint/src/lib.rs b/epaint/src/lib.rs index 17fcd07c..0ccb0bdd 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -87,7 +87,7 @@ pub use { color::{Color32, Rgba}, mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, - shape::{Shape, TextShape}, + shape::{CircleShape, PathShape, RectShape, Shape, TextShape}, stats::PaintStats, stroke::Stroke, tessellator::{TessellationOptions, Tessellator}, @@ -95,6 +95,8 @@ pub use { texture_atlas::{Texture, TextureAtlas}, }; +pub use emath::{pos2, vec2, Pos2, Rect, Vec2}; + pub use ahash; pub use emath; @@ -106,6 +108,7 @@ pub const WHITE_UV: emath::Pos2 = emath::pos2(0.0, 0.0); /// What texture to use in a [`Mesh`] mesh. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub enum TextureId { /// The egui font texture. /// If you don't want to use a texture, pick this and the [`WHITE_UV`] for uv-coord. @@ -122,14 +125,6 @@ impl Default for TextureId { } } -pub(crate) struct PaintRect { - pub rect: emath::Rect, - /// How rounded the corners are. Use `0.0` for no rounding. - pub corner_radius: f32, - pub fill: Color32, - pub stroke: Stroke, -} - /// A [`Shape`] within a clip rectangle. /// /// Everything is using logical points. @@ -146,6 +141,7 @@ pub struct ClippedShape( /// /// Everything is using logical points. #[derive(Clone, Debug)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct ClippedMesh( /// Clip / scissor rectangle. /// Only show the part of the [`Mesh`] that falls within this. diff --git a/epaint/src/mesh.rs b/epaint/src/mesh.rs index 68b9f5fc..1deff677 100644 --- a/epaint/src/mesh.rs +++ b/epaint/src/mesh.rs @@ -6,6 +6,7 @@ use emath::*; /// Should be friendly to send to GPU as is. #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct Vertex { /// Logical pixel coordinates (points). /// (0,0) is the top left corner of the screen. @@ -22,6 +23,7 @@ pub struct Vertex { /// Textured triangles in two dimensions. #[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct Mesh { /// Draw as triangles (i.e. the length is always multiple of three). /// diff --git a/epaint/src/shadow.rs b/epaint/src/shadow.rs index ebb1bcb8..a9e8ab8e 100644 --- a/epaint/src/shadow.rs +++ b/epaint/src/shadow.rs @@ -52,12 +52,11 @@ impl Shadow { let Self { extrusion, color } = *self; use crate::tessellator::*; - let rect = PaintRect { - rect: rect.expand(0.5 * extrusion), - corner_radius: corner_radius + 0.5 * extrusion, - fill: color, - stroke: Default::default(), - }; + let rect = RectShape::filled( + rect.expand(0.5 * extrusion), + corner_radius + 0.5 * extrusion, + color, + ); let mut tessellator = Tessellator::from_options(TessellationOptions { aa_size: extrusion, anti_alias: true, diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index 168e133b..f6161aaf 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -14,32 +14,13 @@ pub enum Shape { /// Recursively nest more shapes - sometimes a convenience to be able to do. /// For performance reasons it is better to avoid it. Vec(Vec), - Circle { - center: Pos2, - radius: f32, - fill: Color32, - stroke: Stroke, - }, + Circle(CircleShape), LineSegment { points: [Pos2; 2], stroke: Stroke, }, - Path { - points: Vec, - /// If true, connect the first and last of the points together. - /// This is required if `fill != TRANSPARENT`. - closed: bool, - /// Fill is only supported for convex polygons. - fill: Color32, - stroke: Stroke, - }, - Rect { - rect: Rect, - /// How rounded the corners are. Use `0.0` for no rounding. - corner_radius: f32, - fill: Color32, - stroke: Stroke, - }, + Path(PathShape), + Rect(RectShape), Text(TextShape), Mesh(Mesh), } @@ -48,6 +29,7 @@ pub enum Shape { impl Shape { /// A line between two points. /// More efficient than calling [`Self::line`]. + #[inline] pub fn line_segment(points: [Pos2; 2], stroke: impl Into) -> Self { Self::LineSegment { points, @@ -58,23 +40,15 @@ impl Shape { /// A line through many points. /// /// Use [`Self::line_segment`] instead if your line only connects two points. + #[inline] pub fn line(points: Vec, stroke: impl Into) -> Self { - Self::Path { - points, - closed: false, - fill: Default::default(), - stroke: stroke.into(), - } + Self::Path(PathShape::line(points, stroke)) } /// A line that closes back to the start point again. + #[inline] pub fn closed_line(points: Vec, stroke: impl Into) -> Self { - Self::Path { - points, - closed: true, - fill: Default::default(), - stroke: stroke.into(), - } + Self::Path(PathShape::closed_line(points, stroke)) } /// Turn a line into equally spaced dots. @@ -102,53 +76,33 @@ impl Shape { } /// A convex polygon with a fill and optional stroke. + #[inline] pub fn convex_polygon( points: Vec, fill: impl Into, stroke: impl Into, ) -> Self { - Self::Path { - points, - closed: true, - fill: fill.into(), - stroke: stroke.into(), - } + Self::Path(PathShape::convex_polygon(points, fill, stroke)) } + #[inline] pub fn circle_filled(center: Pos2, radius: f32, fill_color: impl Into) -> Self { - Self::Circle { - center, - radius, - fill: fill_color.into(), - stroke: Default::default(), - } + Self::Circle(CircleShape::filled(center, radius, fill_color)) } + #[inline] pub fn circle_stroke(center: Pos2, radius: f32, stroke: impl Into) -> Self { - Self::Circle { - center, - radius, - fill: Default::default(), - stroke: stroke.into(), - } + Self::Circle(CircleShape::stroke(center, radius, stroke)) } + #[inline] pub fn rect_filled(rect: Rect, corner_radius: f32, fill_color: impl Into) -> Self { - Self::Rect { - rect, - corner_radius, - fill: fill_color.into(), - stroke: Default::default(), - } + Self::Rect(RectShape::filled(rect, corner_radius, fill_color)) } + #[inline] pub fn rect_stroke(rect: Rect, corner_radius: f32, stroke: impl Into) -> Self { - Self::Rect { - rect, - corner_radius, - fill: Default::default(), - stroke: stroke.into(), - } + Self::Rect(RectShape::stroke(rect, corner_radius, stroke)) } #[allow(clippy::needless_pass_by_value)] @@ -165,6 +119,7 @@ impl Shape { Self::galley(rect.min, galley) } + #[inline] pub fn galley(pos: Pos2, galley: std::sync::Arc) -> Self { TextShape::new(pos, galley).into() } @@ -172,7 +127,165 @@ impl Shape { // ---------------------------------------------------------------------------- -/// How to draw some text on screen. +/// How to paint a circle. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +pub struct CircleShape { + pub center: Pos2, + pub radius: f32, + pub fill: Color32, + pub stroke: Stroke, +} + +impl CircleShape { + #[inline] + pub fn filled(center: Pos2, radius: f32, fill_color: impl Into) -> Self { + Self { + center, + radius, + fill: fill_color.into(), + stroke: Default::default(), + } + } + + #[inline] + pub fn stroke(center: Pos2, radius: f32, stroke: impl Into) -> Self { + Self { + center, + radius, + fill: Default::default(), + stroke: stroke.into(), + } + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: CircleShape) -> Self { + Self::Circle(shape) + } +} + +// ---------------------------------------------------------------------------- + +/// A path which can be stroked and/or filled (if closed). +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +pub struct PathShape { + pub points: Vec, + /// If true, connect the first and last of the points together. + /// This is required if `fill != TRANSPARENT`. + pub closed: bool, + /// Fill is only supported for convex polygons. + pub fill: Color32, + pub stroke: Stroke, +} + +impl PathShape { + /// A line through many points. + /// + /// Use [`Shape::line_segment`] instead if your line only connects two points. + #[inline] + pub fn line(points: Vec, stroke: impl Into) -> Self { + PathShape { + points, + closed: false, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// A line that closes back to the start point again. + #[inline] + pub fn closed_line(points: Vec, stroke: impl Into) -> Self { + PathShape { + points, + closed: true, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// A convex polygon with a fill and optional stroke. + #[inline] + pub fn convex_polygon( + points: Vec, + fill: impl Into, + stroke: impl Into, + ) -> Self { + PathShape { + points, + closed: true, + fill: fill.into(), + stroke: stroke.into(), + } + } + + /// Screen-space bounding rectangle. + #[inline] + pub fn bounding_rect(&self) -> Rect { + Rect::from_points(&self.points).expand(self.stroke.width) + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: PathShape) -> Self { + Self::Path(shape) + } +} + +// ---------------------------------------------------------------------------- + +/// How to paint a rectangle. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +pub struct RectShape { + pub rect: Rect, + /// How rounded the corners are. Use `0.0` for no rounding. + pub corner_radius: f32, + pub fill: Color32, + pub stroke: Stroke, +} + +impl RectShape { + #[inline] + pub fn filled(rect: Rect, corner_radius: f32, fill_color: impl Into) -> Self { + Self { + rect, + corner_radius, + fill: fill_color.into(), + stroke: Default::default(), + } + } + + #[inline] + pub fn stroke(rect: Rect, corner_radius: f32, stroke: impl Into) -> Self { + Self { + rect, + corner_radius, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// Screen-space bounding rectangle. + #[inline] + pub fn bounding_rect(&self) -> Rect { + self.rect.expand(self.stroke.width) + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: RectShape) -> Self { + Self::Rect(shape) + } +} + +// ---------------------------------------------------------------------------- + +/// How to paint some text on screen. #[derive(Clone, Debug, PartialEq)] pub struct TextShape { /// Top left corner of the first character. @@ -206,12 +319,18 @@ impl TextShape { angle: 0.0, } } + + /// Screen-space bounding rectangle. + #[inline] + pub fn bounding_rect(&self) -> Rect { + self.galley.mesh_bounds.translate(self.pos.to_vec2()) + } } impl From for Shape { #[inline(always)] - fn from(text_shape: TextShape) -> Self { - Self::Text(text_shape) + fn from(shape: TextShape) -> Self { + Self::Text(shape) } } @@ -259,7 +378,7 @@ fn dashes_from_line( let new_point = start + vector * (position_on_segment / segment_length); if drawing_dash { // This is the end point. - if let Shape::Path { points, .. } = shapes.last_mut().unwrap() { + if let Shape::Path(PathShape { points, .. }) = shapes.last_mut().unwrap() { points.push(new_point); } position_on_segment += gap_length; @@ -272,7 +391,7 @@ fn dashes_from_line( } // If the segment ends and the dash is not finished, add the segment's end point. if drawing_dash { - if let Shape::Path { points, .. } = shapes.last_mut().unwrap() { + if let Shape::Path(PathShape { points, .. }) = shapes.last_mut().unwrap() { points.push(end); } } @@ -305,21 +424,21 @@ impl Shape { shape.translate(delta); } } - Shape::Circle { center, .. } => { - *center += delta; + Shape::Circle(circle_shape) => { + circle_shape.center += delta; } Shape::LineSegment { points, .. } => { for p in points { *p += delta; } } - Shape::Path { points, .. } => { - for p in points { + Shape::Path(path_shape) => { + for p in &mut path_shape.points { *p += delta; } } - Shape::Rect { rect, .. } => { - *rect = rect.translate(delta); + Shape::Rect(rect_shape) => { + rect_shape.rect = rect_shape.rect.translate(delta); } Shape::Text(text_shape) => { text_shape.pos += delta; diff --git a/epaint/src/shape_transform.rs b/epaint/src/shape_transform.rs index 7c41a211..6215c2d2 100644 --- a/epaint/src/shape_transform.rs +++ b/epaint/src/shape_transform.rs @@ -9,20 +9,20 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { adjust_colors(shape, adjust_color) } } - Shape::Circle { fill, stroke, .. } => { - adjust_color(fill); - adjust_color(&mut stroke.color); + Shape::Circle(circle_shape) => { + adjust_color(&mut circle_shape.fill); + adjust_color(&mut circle_shape.stroke.color); } Shape::LineSegment { stroke, .. } => { adjust_color(&mut stroke.color); } - Shape::Path { fill, stroke, .. } => { - adjust_color(fill); - adjust_color(&mut stroke.color); + Shape::Path(path_shape) => { + adjust_color(&mut path_shape.fill); + adjust_color(&mut path_shape.stroke.color); } - Shape::Rect { fill, stroke, .. } => { - adjust_color(fill); - adjust_color(&mut stroke.color); + Shape::Rect(rect_shape) => { + adjust_color(&mut rect_shape.fill); + adjust_color(&mut rect_shape.stroke.color); } Shape::Text(text_shape) => { if let Some(override_text_color) = &mut text_shape.override_text_color { diff --git a/epaint/src/stats.rs b/epaint/src/stats.rs index ef7c5c79..2f3b334b 100644 --- a/epaint/src/stats.rs +++ b/epaint/src/stats.rs @@ -163,6 +163,9 @@ pub struct PaintStats { pub shape_mesh: AllocInfo, pub shape_vec: AllocInfo, + pub text_shape_vertices: AllocInfo, + pub text_shape_indices: AllocInfo, + /// Number of separate clip rectangles pub clipped_meshes: AllocInfo, pub vertices: AllocInfo, @@ -195,11 +198,16 @@ impl PaintStats { Shape::Noop | Shape::Circle { .. } | Shape::LineSegment { .. } | Shape::Rect { .. } => { Default::default() } - Shape::Path { points, .. } => { - self.shape_path += AllocInfo::from_slice(points); + Shape::Path(path_shape) => { + self.shape_path += AllocInfo::from_slice(&path_shape.points); } Shape::Text(text_shape) => { self.shape_text += AllocInfo::from_galley(&text_shape.galley); + + for row in &text_shape.galley.rows { + self.text_shape_indices += AllocInfo::from_slice(&row.visuals.mesh.indices); + self.text_shape_vertices += AllocInfo::from_slice(&row.visuals.mesh.vertices); + } } Shape::Mesh(mesh) => { self.shape_mesh += AllocInfo::from_mesh(mesh); diff --git a/epaint/src/stroke.rs b/epaint/src/stroke.rs index 371ccc10..ffe34f46 100644 --- a/epaint/src/stroke.rs +++ b/epaint/src/stroke.rs @@ -26,6 +26,12 @@ impl Stroke { color: color.into(), } } + + /// True if width is zero or color is transparent + #[inline] + pub fn is_empty(&self) -> bool { + self.width <= 0.0 || self.color == Color32::TRANSPARENT + } } impl From<(f32, Color)> for Stroke diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index 2a70bdac..3c8c3f89 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -541,12 +541,12 @@ impl Tessellator { self.tessellate_shape(tex_size, shape, out) } } - Shape::Circle { + Shape::Circle(CircleShape { center, radius, fill, stroke, - } => { + }) => { if radius <= 0.0 { return; } @@ -563,70 +563,45 @@ impl Tessellator { self.scratchpad_path.stroke_closed(stroke, options, out); } Shape::Mesh(mesh) => { - if mesh.is_valid() { - out.append(mesh); - } else { + if !mesh.is_valid() { crate::epaint_assert!(false, "Invalid Mesh in Shape::Mesh"); + return; } + + if options.coarse_tessellation_culling && !clip_rect.intersects(mesh.calc_bounds()) + { + return; + } + + out.append(mesh); } Shape::LineSegment { points, stroke } => { + if stroke.is_empty() { + return; + } + + if options.coarse_tessellation_culling + && !clip_rect + .intersects(Rect::from_two_pos(points[0], points[1]).expand(stroke.width)) + { + return; + } + self.scratchpad_path.clear(); self.scratchpad_path.add_line_segment(points); self.scratchpad_path.stroke_open(stroke, options, out); } - Shape::Path { - points, - closed, - fill, - stroke, - } => { - if points.len() >= 2 { - self.scratchpad_path.clear(); - if closed { - self.scratchpad_path.add_line_loop(&points); - } else { - self.scratchpad_path.add_open_points(&points); - } - - if fill != Color32::TRANSPARENT { - crate::epaint_assert!( - closed, - "You asked to fill a path that is not closed. That makes no sense." - ); - self.scratchpad_path.fill(fill, options, out); - } - let typ = if closed { - PathType::Closed - } else { - PathType::Open - }; - self.scratchpad_path.stroke(typ, stroke, options, out); - } + Shape::Path(path_shape) => { + self.tessellate_path(path_shape, out); } - Shape::Rect { - rect, - corner_radius, - fill, - stroke, - } => { - let rect = PaintRect { - rect, - corner_radius, - fill, - stroke, - }; - self.tessellate_rect(&rect, out); + Shape::Rect(rect_shape) => { + self.tessellate_rect(&rect_shape, out); } Shape::Text(text_shape) => { if options.debug_paint_text_rects { + let rect = text_shape.galley.rect.translate(text_shape.pos.to_vec2()); self.tessellate_rect( - &PaintRect { - rect: Rect::from_min_size(text_shape.pos, text_shape.galley.size()) - .expand(0.5), - corner_radius: 2.0, - fill: Default::default(), - stroke: (0.5, Color32::GREEN).into(), - }, + &RectShape::stroke(rect.expand(0.5), 2.0, (0.5, Color32::GREEN)), out, ); } @@ -635,8 +610,48 @@ impl Tessellator { } } - pub(crate) fn tessellate_rect(&mut self, rect: &PaintRect, out: &mut Mesh) { - let PaintRect { + pub(crate) fn tessellate_path(&mut self, path_shape: PathShape, out: &mut Mesh) { + if path_shape.points.len() < 2 { + return; + } + + if self.options.coarse_tessellation_culling + && !path_shape.bounding_rect().intersects(self.clip_rect) + { + return; + } + + let PathShape { + points, + closed, + fill, + stroke, + } = path_shape; + + self.scratchpad_path.clear(); + if closed { + self.scratchpad_path.add_line_loop(&points); + } else { + self.scratchpad_path.add_open_points(&points); + } + + if fill != Color32::TRANSPARENT { + crate::epaint_assert!( + closed, + "You asked to fill a path that is not closed. That makes no sense." + ); + self.scratchpad_path.fill(fill, self.options, out); + } + let typ = if closed { + PathType::Closed + } else { + PathType::Open + }; + self.scratchpad_path.stroke(typ, stroke, self.options, out); + } + + pub(crate) fn tessellate_rect(&mut self, rect: &RectShape, out: &mut Mesh) { + let RectShape { mut rect, corner_radius, fill, @@ -803,12 +818,11 @@ pub fn tessellate_shapes( tessellator.clip_rect = Rect::EVERYTHING; tessellator.tessellate_shape( tex_size, - Shape::Rect { - rect: *clip_rect, - corner_radius: 0.0, - fill: Default::default(), - stroke: Stroke::new(2.0, Color32::from_rgb(150, 255, 150)), - }, + Shape::rect_stroke( + *clip_rect, + 0.0, + Stroke::new(2.0, Color32::from_rgb(150, 255, 150)), + ), mesh, ) } diff --git a/epaint/src/text/fonts.rs b/epaint/src/text/fonts.rs index c3f0dfeb..fe204fa9 100644 --- a/epaint/src/text/fonts.rs +++ b/epaint/src/text/fonts.rs @@ -118,7 +118,6 @@ pub struct FontDefinitions { /// /// `epaint` has built-in-default for these, /// but you can override them if you like. - #[cfg_attr(feature = "persistence", serde(skip))] pub font_data: BTreeMap, /// Which fonts (names) to use for each [`FontFamily`]. @@ -219,7 +218,7 @@ pub struct Fonts { } impl Fonts { - pub fn from_definitions(pixels_per_point: f32, definitions: FontDefinitions) -> Self { + pub fn new(pixels_per_point: f32, definitions: FontDefinitions) -> Self { assert!( 0.0 < pixels_per_point && pixels_per_point < 100.0, "pixels_per_point out of range: {}", @@ -278,6 +277,11 @@ impl Fonts { } } + #[deprecated = "Renamed to Fonts::new"] + pub fn from_definitions(pixels_per_point: f32, definitions: FontDefinitions) -> Self { + Self::new(pixels_per_point, definitions) + } + #[inline(always)] pub fn pixels_per_point(&self) -> f32 { self.pixels_per_point diff --git a/epaint/src/text/text_layout.rs b/epaint/src/text/text_layout.rs index 90b51259..b666c4ca 100644 --- a/epaint/src/text/text_layout.rs +++ b/epaint/src/text/text_layout.rs @@ -333,11 +333,13 @@ fn galley_from_rows(fonts: &Fonts, job: Arc, mut rows: Vec) -> G let format_summary = format_summary(&job); + let mut mesh_bounds = Rect::NOTHING; let mut num_vertices = 0; let mut num_indices = 0; for row in &mut rows { row.visuals = tessellate_row(fonts, &job, &format_summary, row); + mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds); num_vertices += row.visuals.mesh.vertices.len(); num_indices += row.visuals.mesh.indices.len(); } @@ -348,6 +350,7 @@ fn galley_from_rows(fonts: &Fonts, job: Arc, mut rows: Vec) -> G job, rows, rect, + mesh_bounds, num_vertices, num_indices, } diff --git a/epaint/src/text/text_layout_types.rs b/epaint/src/text/text_layout_types.rs index 2d18d9ae..f5bf2343 100644 --- a/epaint/src/text/text_layout_types.rs +++ b/epaint/src/text/text_layout_types.rs @@ -13,6 +13,7 @@ use emath::*; /// /// Pass this to [`Fonts::layout_job]` or [`crate::text::layout`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct LayoutJob { /// The complete text of this job, referenced by `LayoutSection`. pub text: String, // TODO: Cow<'static, str> @@ -136,6 +137,7 @@ impl std::hash::Hash for LayoutJob { // ---------------------------------------------------------------------------- #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct LayoutSection { /// Can be used for first row indentation. pub leading_space: f32, @@ -161,6 +163,7 @@ impl std::hash::Hash for LayoutSection { // ---------------------------------------------------------------------------- #[derive(Copy, Clone, Debug, Hash, PartialEq)] +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] pub struct TextFormat { pub style: TextStyle, /// Text color @@ -225,6 +228,10 @@ pub struct Galley { /// * [`Align::RIGHT`]: rect.right() == 0.0 pub rect: Rect, + /// Tight bounding box around all the meshes in all the rows. + /// Can be used for culling. + pub mesh_bounds: Rect, + /// Total number of vertices in all the row meshes. pub num_vertices: usize,