From 6554fbb1517b8a0d7a3fe4fb59b3c83baa4778f3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 19 Dec 2022 10:33:03 +0100 Subject: [PATCH 1/4] epaint: Improve rendering of very thin rectangles --- crates/epaint/CHANGELOG.md | 1 + crates/epaint/src/tessellator.rs | 32 ++++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index 78be61c3..c3e6ea7f 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the epaint crate will be documented in this file. * Improve the look of thin white lines ([#2437](https://github.com/emilk/egui/pull/2437)). * Don't render `\r` (Carriage Return) ([#2452](https://github.com/emilk/egui/pull/2452)). * Fix bug in `Mesh::split_to_u16` ([#2459](https://github.com/emilk/egui/pull/2459)). +* Improve rendering of very thin rectangles. ## 0.20.0 - 2022-12-08 diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 37c89e6f..fd4b2edd 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1309,12 +1309,32 @@ impl Tessellator { rect.min = rect.min.at_least(pos2(-1e7, -1e7)); rect.max = rect.max.at_most(pos2(1e7, 1e7)); - let path = &mut self.scratchpad_path; - path.clear(); - path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); - path.add_line_loop(&self.scratchpad_points); - path.fill(self.feathering, fill, out); - path.stroke_closed(self.feathering, stroke, out); + if rect.width() < self.feathering { + // Very thin - approximate by a vertial line-segment: + let line = [rect.center_top(), rect.center_bottom()]; + if fill != Color32::TRANSPARENT { + self.tessellate_line(line, Stroke::new(rect.width(), fill), out); + } + if !stroke.is_empty() { + self.tessellate_line(line, stroke, out); + } + } else if rect.height() < self.feathering { + // Very thin - approximate by a horizontal line-segment: + let line = [rect.left_center(), rect.right_center()]; + if fill != Color32::TRANSPARENT { + self.tessellate_line(line, Stroke::new(rect.width(), fill), out); + } + if !stroke.is_empty() { + self.tessellate_line(line, stroke, out); + } + } else { + let path = &mut self.scratchpad_path; + path.clear(); + path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); + path.add_line_loop(&self.scratchpad_points); + path.fill(self.feathering, fill, out); + path.stroke_closed(self.feathering, stroke, out); + } } /// Tessellate a single [`TextShape`] into a [`Mesh`]. From 2c9b1304236f54527417a06f0b3d005ce9df7ecb Mon Sep 17 00:00:00 2001 From: francesco-cattoglio Date: Mon, 19 Dec 2022 10:55:27 +0100 Subject: [PATCH 2/4] Add `Memory::any_popup_open()` (#2464) * Added the "any_popup_open()" function * Updated CHANGELOG.md * add PR link to changelog Co-authored-by: Emil Ernerfeldt --- CHANGELOG.md | 1 + crates/egui/src/memory.rs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b2798c4..ab565462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ### Added ⭐ * `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)). * Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider. +* Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)). ### Fixed 🐛 * Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)). diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 271780ea..27b94c7a 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -479,6 +479,10 @@ impl Memory { self.popup == Some(popup_id) || self.everything_is_visible() } + pub fn any_popup_open(&self) -> bool { + self.popup.is_some() || self.everything_is_visible() + } + pub fn open_popup(&mut self, popup_id: Id) { self.popup = Some(popup_id); } From f9728b88db5fa23764a8129b376d431c5b5500ef Mon Sep 17 00:00:00 2001 From: Ivan <68190772+IVAN-MK7@users.noreply.github.com> Date: Mon, 19 Dec 2022 10:55:39 +0100 Subject: [PATCH 3/4] Combobox .wrap(true) width usage (#2472) * Combobox .wrap(true) width fix .wrap(true) does note use all the available width this fix does not change the original .wrap(false) behaviours * Code comment convention Co-authored-by: IVANMK-7 <68190772+IVANMK-7@users.noreply.github.com> --- crates/egui/src/containers/combo_box.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 38e9f8af..9942e899 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -265,21 +265,35 @@ fn combo_box_dyn<'c, R>( let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| { // We don't want to change width when user selects something new let full_minimum_width = if wrap_enabled { - ui.available_width() - ui.spacing().item_spacing.x * 2.0 + // Currently selected value's text will be wrapped if needed, so occupy the available width. + ui.available_width() } else { + // Occupy at least the minimum width assigned to Slider and ComboBox. ui.spacing().slider_width - 2.0 * margin.x }; let icon_size = Vec2::splat(ui.spacing().icon_width); let wrap_width = if wrap_enabled { + // Use the available width, currently selected value's text will be wrapped if exceeds this value. ui.available_width() - ui.spacing().item_spacing.x - icon_size.x } else { + // Use all the width necessary to display the currently selected value's text. f32::INFINITY }; let galley = selected_text.into_galley(ui, Some(wrap_enabled), wrap_width, TextStyle::Button); - let width = galley.size().x + ui.spacing().item_spacing.x + icon_size.x; + // The width necessary to contain the whole widget with the currently selected value's text. + let width = if wrap_enabled { + full_minimum_width + } else { + // Occupy at least the minimum width needed to contain the widget with the currently selected value's text. + galley.size().x + ui.spacing().item_spacing.x + icon_size.x + }; + + // Case : wrap_enabled : occupy all the available width. + // Case : !wrap_enabled : occupy at least the minimum width assigned to Slider and ComboBox, + // increase if the currently selected value needs additional horizontal space to fully display its text (up to wrap_width (f32::INFINITY)). let width = width.at_least(full_minimum_width); let height = galley.size().y.max(icon_size.y); From 7e9c7dac41128669db0dae57b55a3bbbdde54d10 Mon Sep 17 00:00:00 2001 From: Sven Niederberger <73159570+s-nie@users.noreply.github.com> Date: Mon, 19 Dec 2022 10:57:30 +0100 Subject: [PATCH 4/4] Improve plot grid appearance (#2412) * improve plot grid appearance * update changelog * Update crates/egui/src/widgets/plot/mod.rs Co-authored-by: Emil Ernerfeldt * address review comments * make lines a bit weaker * move changelog entry Co-authored-by: Emil Ernerfeldt --- CHANGELOG.md | 1 + crates/egui/src/widgets/plot/mod.rs | 94 +++++++++++++++++++++-------- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab565462..2d513762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ### Added ⭐ * `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)). * Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider. +* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). * Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)). ### Fixed 🐛 diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 91c0fb66..2978f0dd 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -292,6 +292,7 @@ pub struct Plot { show_background: bool, show_axes: [bool; 2], grid_spacers: [GridSpacer; 2], + sharp_grid_lines: bool, } impl Plot { @@ -331,6 +332,7 @@ impl Plot { show_background: true, show_axes: [true; 2], grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], + sharp_grid_lines: true, } } @@ -617,6 +619,13 @@ impl Plot { self } + /// Round grid positions to full pixels to avoid aliasing. Improves plot appearance but might have an + /// undesired effect when shifting the plot bounds. Enabled by default. + pub fn sharp_grid_lines(mut self, enabled: bool) -> Self { + self.sharp_grid_lines = enabled; + self + } + /// Resets the plot. pub fn reset(mut self) -> Self { self.reset = true; @@ -663,6 +672,7 @@ impl Plot { linked_axes, linked_cursors, grid_spacers, + sharp_grid_lines, } = self; // Determine the size of the plot in the UI @@ -965,6 +975,7 @@ impl Plot { draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x), draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y), draw_cursors, + sharp_grid_lines, }; let plot_cursors = prepared.ui(ui, &response); @@ -1290,18 +1301,30 @@ struct PreparedPlot { draw_cursor_x: bool, draw_cursor_y: bool, draw_cursors: Vec, + sharp_grid_lines: bool, } impl PreparedPlot { fn ui(self, ui: &mut Ui, response: &Response) -> Vec { - let mut shapes = Vec::new(); + let mut axes_shapes = Vec::new(); for d in 0..2 { if self.show_axes[d] { - self.paint_axis(ui, d, &mut shapes); + self.paint_axis( + ui, + d, + self.show_axes[1 - d], + &mut axes_shapes, + self.sharp_grid_lines, + ); } } + // Sort the axes by strength so that those with higher strength are drawn in front. + axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2)); + + let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect(); + let transform = &self.transform; let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default()); @@ -1369,7 +1392,14 @@ impl PreparedPlot { cursors } - fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { + fn paint_axis( + &self, + ui: &Ui, + axis: usize, + other_axis_shown: bool, + shapes: &mut Vec<(Shape, f32)>, + sharp_grid_lines: bool, + ) { let Self { transform, axis_formatters, @@ -1408,29 +1438,35 @@ impl PreparedPlot { let pos_in_gui = transform.position_from_point(&value); let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32; - let line_alpha = remap_clamp( - spacing_in_points, - (MIN_LINE_SPACING_IN_POINTS as f32)..=300.0, - 0.0..=0.15, - ); + if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 { + let line_strength = remap_clamp( + spacing_in_points, + MIN_LINE_SPACING_IN_POINTS as f32..=300.0, + 0.0..=1.0, + ); - if line_alpha > 0.0 { - let line_color = color_from_alpha(ui, line_alpha); + let line_color = color_from_contrast(ui, line_strength); let mut p0 = pos_in_gui; let mut p1 = pos_in_gui; p0[1 - axis] = transform.frame().min[1 - axis]; p1[1 - axis] = transform.frame().max[1 - axis]; - // Round to avoid aliasing - p0 = ui.ctx().round_pos_to_pixels(p0); - p1 = ui.ctx().round_pos_to_pixels(p1); - shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, line_color))); + if sharp_grid_lines { + // Round to avoid aliasing + p0 = ui.ctx().round_pos_to_pixels(p0); + p1 = ui.ctx().round_pos_to_pixels(p1); + } + shapes.push(( + Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)), + line_strength, + )); } - let text_alpha = remap_clamp(spacing_in_points, 40.0..=150.0, 0.0..=0.4); - - if text_alpha > 0.0 { - let color = color_from_alpha(ui, text_alpha); + const MIN_TEXT_SPACING: f32 = 40.0; + if spacing_in_points > MIN_TEXT_SPACING { + let text_strength = + remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0); + let color = color_from_contrast(ui, text_strength); let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() { formatter(value_main, &axis_range) @@ -1438,8 +1474,11 @@ impl PreparedPlot { emath::round_to_decimals(value_main, 5).to_string() // hack }; + // Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice) + let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0; + // Custom formatters can return empty string to signal "no label at this resolution" - if !text.is_empty() { + if !text.is_empty() && !skip_origin_y { let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color); let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y); @@ -1449,17 +1488,20 @@ impl PreparedPlot { .at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0) .at_least(transform.frame().min[1 - axis] + 1.0); - shapes.push(Shape::galley(text_pos, galley)); + shapes.push((Shape::galley(text_pos, galley), text_strength)); } } } - fn color_from_alpha(ui: &Ui, alpha: f32) -> Color32 { - if ui.visuals().dark_mode { - Rgba::from_white_alpha(alpha).into() - } else { - Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into() - } + fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 { + let bg = ui.visuals().extreme_bg_color; + let fg = ui.visuals().widgets.open.fg_stroke.color; + let mix = 0.5 * contrast.sqrt(); + Color32::from_rgb( + lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8, + lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8, + lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8, + ) } }