From 3777b8d2741f298eaa1409dc08062902f7541990 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 29 Aug 2024 10:38:19 +0200 Subject: [PATCH 1/3] Truncate text in clipped `Table` columns (#5023) * Closes https://github.com/emilk/egui/issues/5013 * Columns with `clip = true` will have `TextWrapMode::Truncate` set * Added setting `Column::auto_size_this_frame` (acts like a double-click on column resizer) * Set `sizing_pass` on all cells in a column that is being auto-sized (e.g. on double-click) ![image](https://github.com/user-attachments/assets/360f6b59-c9a9-468b-8919-4b7e4fc6661a) --- crates/egui/src/ui_builder.rs | 2 +- crates/egui_extras/src/layout.rs | 25 ++++++++---- crates/egui_extras/src/table.rs | 70 ++++++++++++++++++++++---------- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 7f2e8b92478..166210b9ab2 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -11,7 +11,7 @@ use crate::Ui; /// except for `max_rect` which by default is set to /// the parent [`Ui::available_rect_before_wrap`]. #[must_use] -#[derive(Default)] +#[derive(Clone, Default)] pub struct UiBuilder { pub id_source: Option, pub ui_stack_info: UiStackInfo, diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 6eb951c8552..6d2b10c3092 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -33,6 +33,9 @@ pub(crate) struct StripLayoutFlags { pub(crate) striped: bool, pub(crate) hovered: bool, pub(crate) selected: bool, + + /// Used when we want to accruately measure the size of this cell. + pub(crate) sizing_pass: bool, } /// Positions cells in [`CellDirection`] and starts a new line on [`StripLayout::end_line`] @@ -197,19 +200,27 @@ impl<'l> StripLayout<'l> { child_ui_id_source: egui::Id, add_cell_contents: impl FnOnce(&mut Ui), ) -> Ui { - let mut child_ui = self.ui.new_child( - UiBuilder::new() - .id_source(child_ui_id_source) - .ui_stack_info(egui::UiStackInfo::new(egui::UiKind::TableCell)) - .max_rect(max_rect) - .layout(self.cell_layout), - ); + let mut ui_builder = UiBuilder::new() + .id_source(child_ui_id_source) + .ui_stack_info(egui::UiStackInfo::new(egui::UiKind::TableCell)) + .max_rect(max_rect) + .layout(self.cell_layout); + if flags.sizing_pass { + ui_builder = ui_builder.sizing_pass(); + } + + let mut child_ui = self.ui.new_child(ui_builder); if flags.clip { let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin); let margin = margin.min(0.5 * self.ui.spacing().item_spacing); let clip_rect = max_rect.expand2(margin); child_ui.set_clip_rect(clip_rect.intersect(child_ui.clip_rect())); + + if !child_ui.is_sizing_pass() { + // Better to truncate (if we can), rather than hard clipping: + child_ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + } } if flags.selected { diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 999dcfe7d5b..f2b2dfe48ac 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -38,6 +38,10 @@ pub struct Column { clip: bool, resizable: Option, + + /// If set, we should acurately measure the size of this column this frame + /// so that we can correctly auto-size it. This is done as a `sizing_pass`. + auto_size_this_frame: bool, } impl Column { @@ -86,6 +90,7 @@ impl Column { width_range: Rangef::new(0.0, f32::INFINITY), resizable: None, clip: false, + auto_size_this_frame: false, } } @@ -138,6 +143,15 @@ impl Column { self } + /// If set, the column will be automatically sized based on the content this frame. + /// + /// Do not set this every frame, just on a specific action. + #[inline] + pub fn auto_size_this_frame(mut self, auto_size_this_frame: bool) -> Self { + self.auto_size_this_frame = auto_size_this_frame; + self + } + fn is_auto(&self) -> bool { match self.initial_width { InitialColumnSize::Automatic(_) => true, @@ -446,10 +460,11 @@ impl<'a> TableBuilder<'a> { let mut max_used_widths = vec![0.0; columns.len()]; let table_top = ui.cursor().top(); - ui.scope(|ui| { - if is_sizing_pass { - ui.set_sizing_pass(); - } + let mut ui_builder = egui::UiBuilder::new(); + if is_sizing_pass { + ui_builder = ui_builder.sizing_pass(); + } + ui.scope_builder(ui_builder, |ui| { let mut layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout, sense); let mut response: Option = None; add_header_row(TableRow { @@ -671,7 +686,7 @@ impl<'a> Table<'a> { ui, table_top, state_id, - columns, + mut columns, resizable, mut available_width, mut state, @@ -695,6 +710,15 @@ impl<'a> Table<'a> { scroll_bar_visibility, } = scroll_options; + for (i, column) in columns.iter_mut().enumerate() { + let column_resize_id = ui.id().with("resize_column").with(i); + if let Some(response) = ui.ctx().read_response(column_resize_id) { + if response.double_clicked() { + column.auto_size_this_frame = true; + } + } + } + let cursor_position = ui.cursor().min; let mut scroll_area = ScrollArea::new([false, vscroll]) @@ -718,11 +742,11 @@ impl<'a> Table<'a> { let clip_rect = ui.clip_rect(); - ui.scope(|ui| { - if is_sizing_pass { - ui.set_sizing_pass(); - } - + let mut ui_builder = egui::UiBuilder::new(); + if is_sizing_pass { + ui_builder = ui_builder.sizing_pass(); + } + ui.scope_builder(ui_builder, |ui| { let hovered_row_index_id = self.state_id.with("__table_hovered_row"); let hovered_row_index = ui.data_mut(|data| data.remove_temp::(hovered_row_index_id)); @@ -736,8 +760,7 @@ impl<'a> Table<'a> { max_used_widths: max_used_widths_ref, striped, row_index: 0, - start_y: clip_rect.top(), - end_y: clip_rect.bottom(), + y_range: clip_rect.y_range(), scroll_to_row: scroll_to_row.map(|(r, _)| r), scroll_to_y_range: &mut scroll_to_y_range, hovered_row_index, @@ -745,7 +768,7 @@ impl<'a> Table<'a> { }); if scroll_to_row.is_some() && scroll_to_y_range.is_none() { - // TableBody::row didn't find the right row, so scroll to the bottom: + // TableBody::row didn't find the correct row, so scroll to the bottom: scroll_to_y_range = Some(Rangef::new(f32::INFINITY, f32::INFINITY)); } }); @@ -811,9 +834,8 @@ impl<'a> Table<'a> { let resize_response = ui.interact(line_rect, column_resize_id, egui::Sense::click_and_drag()); - if resize_response.double_clicked() { - // Resize to the minimum of what is needed. - + if column.auto_size_this_frame { + // Auto-size: resize to what is needed. *column_width = width_range.clamp(max_used_widths[i]); } else if resize_response.dragged() { if let Some(pointer) = ui.ctx().pointer_latest_pos() { @@ -884,8 +906,7 @@ pub struct TableBody<'a> { striped: bool, row_index: usize, - start_y: f32, - end_y: f32, + y_range: Rangef, /// Look for this row to scroll to. scroll_to_row: Option, @@ -916,7 +937,7 @@ impl<'a> TableBody<'a> { } fn scroll_offset_y(&self) -> f32 { - self.start_y - self.layout.rect.top() + self.y_range.min - self.layout.rect.top() } /// Return a vector containing all column widths for this table body. @@ -1002,7 +1023,7 @@ impl<'a> TableBody<'a> { let scroll_offset_y = self .scroll_offset_y() .min(total_rows as f32 * row_height_with_spacing); - let max_height = self.end_y - self.start_y; + let max_height = self.y_range.span(); let mut min_row = 0; if scroll_offset_y > 0.0 { @@ -1074,7 +1095,7 @@ impl<'a> TableBody<'a> { let spacing = self.layout.ui.spacing().item_spacing; let mut enumerated_heights = heights.enumerate(); - let max_height = self.end_y - self.start_y; + let max_height = self.y_range.span(); let scroll_offset_y = self.scroll_offset_y() as f64; let scroll_to_y_range_offset = self.layout.cursor.y as f64; @@ -1221,7 +1242,7 @@ pub struct TableRow<'a, 'b> { } impl<'a, 'b> TableRow<'a, 'b> { - /// Add the contents of a column. + /// Add the contents of a column on this row (i.e. a cell). /// /// Returns the used space (`min_rect`) plus the [`Response`] of the whole cell. #[cfg_attr(debug_assertions, track_caller)] @@ -1229,6 +1250,10 @@ impl<'a, 'b> TableRow<'a, 'b> { let col_index = self.col_index; let clip = self.columns.get(col_index).map_or(false, |c| c.clip); + let auto_size_this_frame = self + .columns + .get(col_index) + .map_or(false, |c| c.auto_size_this_frame); let width = if let Some(width) = self.widths.get(col_index) { self.col_index += 1; @@ -1249,6 +1274,7 @@ impl<'a, 'b> TableRow<'a, 'b> { striped: self.striped, hovered: self.hovered, selected: self.selected, + sizing_pass: auto_size_this_frame || self.layout.ui.is_sizing_pass(), }; let (used_rect, response) = self.layout.add( From f2815b423eb0b04dc58c7cc7086526bb2d3e0b6c Mon Sep 17 00:00:00 2001 From: Juan Campa Date: Fri, 30 Aug 2024 03:57:32 -0400 Subject: [PATCH 2/3] Fix blurry lines (#4943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Closes * [x] I have followed the instructions in the PR template I've been meaning to look into this for a while but finally bit the bullet this week. Contrary to what I initially thought, the problem of blurry lines is unrelated to feathering because it also happens with feathering disabled. The root cause is that lines tend to land on pixel boundaries, and because of that, frequently used strokes (e.g. 1pt), end up partially covering pixels. This is especially noticeable on 1ppp displays. There were a couple of things to fix, namely: individual lines like separators and indents but also shape strokes (e.g. Frame). Lines were easy, I just made sure we round them to the nearest pixel _center_, instead of the nearest pixel boundary. Strokes were a little more complicated. To illustrate why, here’s an example: if we're rendering a 5x5 rect (black fill, red stroke), we would expect to see something like this: ![Screenshot 2024-08-11 at 15 01 41](https://github.com/user-attachments/assets/5a5d4434-0814-451b-8179-2864dc73c6a6) The fill and the stroke to cover entire pixels. Instead, egui was painting the stroke partially inside and partially outside, centered around the shape’s path (blue line): ![Screenshot 2024-08-11 at 15 00 57](https://github.com/user-attachments/assets/4284dc91-5b6e-4422-994a-17d527a6f13b) Both methods are valid for different use-cases but the first one is what we’d typically want for UIs to feel crisp and pixel perfect. It's also how CSS borders work (related to #4019 and #3284). Luckily, we can use the normal computed for each `PathPoint` to adjust the location of the stroke to be outside, inside, or in the middle. These also are the 3 types of strokes available in tools like Photoshop. This PR introduces an enum `StrokeKind` which determines if a `PathStroke` should be tessellated outside, inside, or _on_ the path itself. Where "outside" is defined by the directions normals point to. Tessellator will now use `StrokeKind::Outside` for closed shapes like rect, ellipse, etc. And `StrokeKind::Middle` for the rest since there's no meaningful "outside" concept for open paths. This PR doesn't expose `StrokeKind` to user-land, but we can implement that later so that users can render shapes and decide where to place the stroke. ### Strokes test (blue lines represent the size of the rect being rendered) `Stroke::Middle` (current behavior, 1px and 3px are blurry) ![Screenshot 2024-08-09 at 23 55 48](https://github.com/user-attachments/assets/dabeaa9e-2010-4eb6-bd7e-b9cb3660542e) `Stroke::Outside` (proposed default behavior for closed paths) ![Screenshot 2024-08-09 at 23 51 55](https://github.com/user-attachments/assets/509c261f-0ae1-46a0-b9b8-08de31c3bd85) `Stroke::Inside` (for completeness but unused at the moment) ![Screenshot 2024-08-09 at 23 54 49](https://github.com/user-attachments/assets/c011b1c1-60ab-4577-baa9-14c36267438a) ### Demo App The best way to review this PR is to run the demo on a 1ppp display, especially to test hover effects. Everything should look crisper. Also run it in a higher dpi screen to test that nothing broke 🙏. Before: ![egui_old](https://github.com/user-attachments/assets/cd6e9032-d44f-4cb0-bb41-f9eb4c3ae810) After (notice the sharper lines): ![egui_new](https://github.com/user-attachments/assets/3365fc96-6eb2-4e7d-a2f5-b4712625a702) --- crates/egui/src/containers/panel.rs | 28 +++- crates/egui/src/containers/window.rs | 26 +-- crates/egui/src/context.rs | 24 ++- crates/egui/src/painter.rs | 14 +- crates/egui/src/style.rs | 8 +- crates/egui/src/ui.rs | 4 +- crates/egui/src/widgets/separator.rs | 4 +- crates/egui_demo_app/src/wrap_app.rs | 13 +- .../src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/rendering_test.rs | 43 +++++ crates/epaint/src/stroke.rs | 49 ++++++ crates/epaint/src/tessellator.rs | 153 ++++++++++++------ 12 files changed, 278 insertions(+), 89 deletions(-) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index eaffa0be15e..d6c0321f110 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -329,6 +329,9 @@ impl SidePanel { ui.ctx().set_cursor_icon(cursor_icon); } + // Keep this rect snapped so that panel content can be pixel-perfect + let rect = ui.painter().round_rect_to_pixels(rect); + PanelState { rect }.store(ui.ctx(), id); { @@ -343,10 +346,14 @@ impl SidePanel { Stroke::NONE }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done - // In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel - // (hence the shrink). - let resize_x = side.opposite().side_x(rect.shrink(1.0)); - let resize_x = ui.painter().round_to_pixel(resize_x); + let resize_x = side.opposite().side_x(rect); + + // This makes it pixel-perfect for odd-sized strokes (width=1.0, width=3.0, etc) + let resize_x = ui.painter().round_to_pixel_center(resize_x); + + // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for + // left-side panels + let resize_x = resize_x - if side == Side::Left { 1.0 } else { 0.0 }; ui.painter().vline(resize_x, panel_rect.y_range(), stroke); } @@ -817,6 +824,9 @@ impl TopBottomPanel { ui.ctx().set_cursor_icon(cursor_icon); } + // Keep this rect snapped so that panel content can be pixel-perfect + let rect = ui.painter().round_rect_to_pixels(rect); + PanelState { rect }.store(ui.ctx(), id); { @@ -831,10 +841,12 @@ impl TopBottomPanel { Stroke::NONE }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done - // In the meantime: nudge the line so its inside the panel, so it won't be covered by neighboring panel - // (hence the shrink). - let resize_y = side.opposite().side_y(rect.shrink(1.0)); - let resize_y = ui.painter().round_to_pixel(resize_y); + let resize_y = side.opposite().side_y(rect); + let resize_y = ui.painter().round_to_pixel_center(resize_y); + + // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for + // top-side panels + let resize_y = resize_y - if side == TopBottomSide::Top { 1.0 } else { 0.0 }; ui.painter().hline(panel_rect.x_range(), resize_y, stroke); } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index eb7184c6e22..c183a4dc731 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -439,9 +439,6 @@ impl<'open> Window<'open> { let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); // Keep the original inner margin for later use let window_margin = window_frame.inner_margin; - let border_padding = window_frame.stroke.width / 2.0; - // Add border padding to the inner margin to prevent it from covering the contents - window_frame.inner_margin += border_padding; let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); @@ -575,9 +572,9 @@ impl<'open> Window<'open> { if let Some(title_bar) = title_bar { let mut title_rect = Rect::from_min_size( - outer_rect.min + vec2(border_padding, border_padding), + outer_rect.min, Vec2 { - x: outer_rect.size().x - border_padding * 2.0, + x: outer_rect.size().x, y: title_bar_height, }, ); @@ -587,9 +584,6 @@ impl<'open> Window<'open> { if on_top && area_content_ui.visuals().window_highlight_topmost { let mut round = window_frame.rounding; - // Eliminate the rounding gap between the title bar and the window frame - round -= border_padding; - if !is_collapsed { round.se = 0.0; round.sw = 0.0; @@ -603,7 +597,7 @@ impl<'open> Window<'open> { // Fix title bar separator line position if let Some(response) = &mut content_response { - response.rect.min.y = outer_rect.min.y + title_bar_height + border_padding; + response.rect.min.y = outer_rect.min.y + title_bar_height; } title_bar.ui( @@ -667,14 +661,10 @@ fn paint_resize_corner( } }; - // Adjust the corner offset to accommodate the stroke width and window rounding - let offset = if radius <= 2.0 && stroke.width < 2.0 { - 2.0 - } else { - // The corner offset is calculated to make the corner appear to be in the correct position - (2.0_f32.sqrt() * (1.0 + radius + stroke.width / 2.0) - radius) - * 45.0_f32.to_radians().cos() - }; + // Adjust the corner offset to accommodate for window rounding + let offset = + ((2.0_f32.sqrt() * (1.0 + radius) - radius) * 45.0_f32.to_radians().cos()).max(2.0); + let corner_size = Vec2::splat(ui.visuals().resize_corner_size); let corner_rect = corner.align_size_within_rect(corner_size, outer_rect); let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner @@ -1136,7 +1126,6 @@ impl TitleBar { let text_pos = emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top(); let text_pos = text_pos - self.title_galley.rect.min.to_vec2(); - let text_pos = text_pos - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better) ui.painter().galley( text_pos, self.title_galley.clone(), @@ -1150,6 +1139,7 @@ impl TitleBar { let stroke = ui.visuals().widgets.noninteractive.bg_stroke; // Workaround: To prevent border infringement, // the 0.1 value should ideally be calculated using TessellationOptions::feathering_size_in_pixels + // or we could support selectively disabling feathering on line caps let x_range = outer_rect.x_range().shrink(0.1); ui.painter().hline(x_range, y, stroke); } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index ff13c14d764..7225af6d1ca 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1717,26 +1717,42 @@ impl Context { }); } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). + #[inline] + pub(crate) fn round_to_pixel_center(&self, point: f32) -> f32 { + let pixels_per_point = self.pixels_per_point(); + ((point * pixels_per_point - 0.5).round() + 0.5) / pixels_per_point + } + + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). + #[inline] + pub(crate) fn round_pos_to_pixel_center(&self, point: Pos2) -> Pos2 { + pos2( + self.round_to_pixel_center(point.x), + self.round_to_pixel_center(point.y), + ) + } + + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_to_pixel(&self, point: f32) -> f32 { let pixels_per_point = self.pixels_per_point(); (point * pixels_per_point).round() / pixels_per_point } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y)) } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 { vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y)) } - /// Useful for pixel-perfect rendering + /// Useful for pixel-perfect rendering of filled shapes #[inline] pub(crate) fn round_rect_to_pixels(&self, rect: Rect) -> Rect { Rect { diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index b173310f756..a79deac5c9a 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -158,7 +158,19 @@ impl Painter { self.clip_rect = clip_rect; } - /// Useful for pixel-perfect rendering. + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). + #[inline] + pub fn round_to_pixel_center(&self, point: f32) -> f32 { + self.ctx().round_to_pixel_center(point) + } + + /// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels). + #[inline] + pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { + self.ctx().round_pos_to_pixel_center(pos) + } + + /// Useful for pixel-perfect rendering of filled shapes. #[inline] pub fn round_to_pixel(&self, point: f32) -> f32 { self.ctx().round_to_pixel(point) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 46f391c0216..0097f53521f 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2477,8 +2477,12 @@ impl Widget for &mut Stroke { // stroke preview: let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size); - let left = stroke_rect.left_center(); - let right = stroke_rect.right_center(); + let left = ui + .painter() + .round_pos_to_pixel_center(stroke_rect.left_center()); + let right = ui + .painter() + .round_pos_to_pixel_center(stroke_rect.right_center()); ui.painter().line_segment([left, right], (*width, *color)); }) .response diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 8f9454ce45c..e502f11e148 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -2215,9 +2215,9 @@ impl Ui { let stroke = self.visuals().widgets.noninteractive.bg_stroke; let left_top = child_rect.min - 0.5 * indent * Vec2::X; - let left_top = self.painter().round_pos_to_pixels(left_top); + let left_top = self.painter().round_pos_to_pixel_center(left_top); let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0); - let left_bottom = self.painter().round_pos_to_pixels(left_bottom); + let left_bottom = self.painter().round_pos_to_pixel_center(left_bottom); if left_vline { // draw a faint line on the left to mark the indented section diff --git a/crates/egui/src/widgets/separator.rs b/crates/egui/src/widgets/separator.rs index 7792bd23950..e421de9cf89 100644 --- a/crates/egui/src/widgets/separator.rs +++ b/crates/egui/src/widgets/separator.rs @@ -116,12 +116,12 @@ impl Widget for Separator { if is_horizontal_line { painter.hline( (rect.left() - grow)..=(rect.right() + grow), - painter.round_to_pixel(rect.center().y), + painter.round_to_pixel_center(rect.center().y), stroke, ); } else { painter.vline( - painter.round_to_pixel(rect.center().x), + painter.round_to_pixel_center(rect.center().x), (rect.top() - grow)..=(rect.bottom() + grow), stroke, ); diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 13e26f83b45..3805ce9a4cc 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -277,12 +277,14 @@ impl eframe::App for WrapApp { } let mut cmd = Command::Nothing; - egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| { - ui.horizontal_wrapped(|ui| { - ui.visuals_mut().button_frame = false; - self.bar_contents(ui, frame, &mut cmd); + egui::TopBottomPanel::top("wrap_app_top_bar") + .frame(egui::Frame::none().inner_margin(4.0)) + .show(ctx, |ui| { + ui.horizontal_wrapped(|ui| { + ui.visuals_mut().button_frame = false; + self.bar_contents(ui, frame, &mut cmd); + }); }); - }); self.state.backend_panel.update(ctx, frame); @@ -324,6 +326,7 @@ impl WrapApp { egui::SidePanel::left("backend_panel") .resizable(false) .show_animated(ctx, is_open, |ui| { + ui.add_space(4.0); ui.vertical_centered(|ui| { ui.heading("💻 Backend"); }); diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 160ab2e6a60..576a69e66df 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -260,6 +260,7 @@ impl DemoWindows { .resizable(false) .default_width(150.0) .show(ctx, |ui| { + ui.add_space(4.0); ui.vertical_centered(|ui| { ui.heading("✒ egui demos"); }); diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index f3c788a69e3..70fd9983768 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -415,6 +415,49 @@ pub fn pixel_test(ui: &mut Ui) { ui.add_space(4.0); pixel_test_squares(ui); + + ui.add_space(4.0); + + pixel_test_strokes(ui); +} + +fn pixel_test_strokes(ui: &mut Ui) { + ui.label("The strokes should align to the physical pixel grid."); + let color = if ui.style().visuals.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + let pixels_per_point = ui.ctx().pixels_per_point(); + + for thickness_pixels in 1..=3 { + let thickness_pixels = thickness_pixels as f32; + let thickness_points = thickness_pixels / pixels_per_point; + let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32; + let size_pixels = vec2( + ui.available_width(), + num_squares as f32 + thickness_pixels * 2.0, + ); + let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0); + let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); + + let mut cursor_pixel = Pos2::new( + response.rect.min.x * pixels_per_point + thickness_pixels, + response.rect.min.y * pixels_per_point + thickness_pixels, + ) + .ceil(); + + let stroke = Stroke::new(thickness_points, color); + for size in 1..=num_squares { + let rect_points = Rect::from_min_size( + Pos2::new(cursor_pixel.x, cursor_pixel.y), + Vec2::splat(size as f32), + ); + painter.rect_stroke(rect_points / pixels_per_point, 0.0, stroke); + cursor_pixel.x += (1 + size) as f32 + thickness_pixels * 2.0; + } + } } fn pixel_test_squares(ui: &mut Ui) { diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index f1155bd07d6..399a602c259 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -55,6 +55,26 @@ impl std::hash::Hash for Stroke { } } +/// Describes how the stroke of a shape should be painted. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum StrokeKind { + /// The stroke should be painted entirely outside of the shape + Outside, + + /// The stroke should be painted entirely inside of the shape + Inside, + + /// The stroke should be painted right on the edge of the shape, half inside and half outside. + Middle, +} + +impl Default for StrokeKind { + fn default() -> Self { + Self::Middle + } +} + /// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`] /// /// The default stroke is the same as [`Stroke::NONE`]. @@ -63,6 +83,7 @@ impl std::hash::Hash for Stroke { pub struct PathStroke { pub width: f32, pub color: ColorMode, + pub kind: StrokeKind, } impl PathStroke { @@ -70,6 +91,7 @@ impl PathStroke { pub const NONE: Self = Self { width: 0.0, color: ColorMode::TRANSPARENT, + kind: StrokeKind::Middle, }; #[inline] @@ -77,6 +99,7 @@ impl PathStroke { Self { width: width.into(), color: ColorMode::Solid(color.into()), + kind: StrokeKind::default(), } } @@ -91,6 +114,31 @@ impl PathStroke { Self { width: width.into(), color: ColorMode::UV(Arc::new(callback)), + kind: StrokeKind::default(), + } + } + + /// Set the stroke to be painted right on the edge of the shape, half inside and half outside. + pub fn middle(self) -> Self { + Self { + kind: StrokeKind::Middle, + ..self + } + } + + /// Set the stroke to be painted entirely outside of the shape + pub fn outside(self) -> Self { + Self { + kind: StrokeKind::Outside, + ..self + } + } + + /// Set the stroke to be painted entirely inside of the shape + pub fn inside(self) -> Self { + Self { + kind: StrokeKind::Inside, + ..self } } @@ -116,6 +164,7 @@ impl From for PathStroke { Self { width: value.width, color: ColorMode::Solid(value.color), + kind: StrokeKind::default(), } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 6ee1a11a53d..1ef7c471880 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -303,7 +303,7 @@ mod precomputed_vertices { // ---------------------------------------------------------------------------- -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] struct PathPoint { pos: Pos2, @@ -478,23 +478,23 @@ impl Path { } /// Open-ended. - pub fn stroke_open(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { - stroke_path(feathering, &self.0, PathType::Open, stroke, out); + pub fn stroke_open(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { + stroke_path(feathering, &mut self.0, PathType::Open, stroke, out); } /// A closed path (returning to the first point). - pub fn stroke_closed(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { - stroke_path(feathering, &self.0, PathType::Closed, stroke, out); + pub fn stroke_closed(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { + stroke_path(feathering, &mut self.0, PathType::Closed, stroke, out); } pub fn stroke( - &self, + &mut self, feathering: f32, path_type: PathType, stroke: &PathStroke, out: &mut Mesh, ) { - stroke_path(feathering, &self.0, path_type, stroke, out); + stroke_path(feathering, &mut self.0, path_type, stroke, out); } /// The path is taken to be closed (i.e. returning to the start again). @@ -502,8 +502,8 @@ impl Path { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. - pub fn fill(&mut self, feathering: f32, color: Color32, out: &mut Mesh) { - fill_closed_path(feathering, &mut self.0, color, out); + pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) { + fill_closed_path(feathering, &mut self.0, color, stroke, out); } /// Like [`Self::fill`] but with texturing. @@ -536,8 +536,6 @@ pub mod path { let r = clamp_rounding(rounding, rect); if r == Rounding::ZERO { - let min = rect.min; - let max = rect.max; path.reserve(4); path.push(pos2(min.x, min.y)); // left top path.push(pos2(max.x, min.y)); // right top @@ -738,11 +736,31 @@ fn cw_signed_area(path: &[PathPoint]) -> f64 { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. -fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out: &mut Mesh) { +/// +/// A stroke is required so that the fill's feathering can fade to the right color. You can pass `&PathStroke::NONE` if +/// this path won't be stroked. +fn fill_closed_path( + feathering: f32, + path: &mut [PathPoint], + color: Color32, + stroke: &PathStroke, + out: &mut Mesh, +) { if color == Color32::TRANSPARENT { return; } + // TODO(juancampa): This bounding box is computed twice per shape: once here and another when tessellating the + // stroke, consider hoisting that logic to the tessellator/scratchpad. + let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) + .expand((stroke.width / 2.0) + feathering); + + let stroke_color = &stroke.color; + let get_stroke_color: Box Color32> = match stroke_color { + ColorMode::Solid(col) => Box::new(|_pos: Pos2| *col), + ColorMode::UV(fun) => Box::new(|pos: Pos2| fun(bbox, pos)), + }; + let n = path.len() as u32; if feathering > 0.0 { if cw_signed_area(path) < 0.0 { @@ -755,7 +773,6 @@ fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out out.reserve_triangles(3 * n as usize); out.reserve_vertices(2 * n as usize); - let color_outer = Color32::TRANSPARENT; let idx_inner = out.vertices.len() as u32; let idx_outer = idx_inner + 1; @@ -769,8 +786,13 @@ fn fill_closed_path(feathering: f32, path: &mut [PathPoint], color: Color32, out for i1 in 0..n { let p1 = &path[i1 as usize]; let dm = 0.5 * feathering * p1.normal; - out.colored_vertex(p1.pos - dm, color); - out.colored_vertex(p1.pos + dm, color_outer); + + let pos_inner = p1.pos - dm; + let pos_outer = p1.pos + dm; + let color_outer = get_stroke_color(pos_outer); + + out.colored_vertex(pos_inner, color); + out.colored_vertex(pos_outer, color_outer); out.add_triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0); out.add_triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1); i0 = i1; @@ -872,10 +894,24 @@ fn fill_closed_path_with_uv( } } +/// Translate a point along their normals according to the stroke kind. +#[inline(always)] +fn translate_stroke_point(p: &mut PathPoint, stroke: &PathStroke) { + match stroke.kind { + stroke::StrokeKind::Middle => { /* Nothingn to do */ } + stroke::StrokeKind::Outside => { + p.pos += p.normal * stroke.width * 0.5; + } + stroke::StrokeKind::Inside => { + p.pos -= p.normal * stroke.width * 0.5; + } + } +} + /// Tessellate the given path as a stroke with thickness. fn stroke_path( feathering: f32, - path: &[PathPoint], + path: &mut [PathPoint], path_type: PathType, stroke: &PathStroke, out: &mut Mesh, @@ -888,6 +924,12 @@ fn stroke_path( let idx = out.vertices.len() as u32; + // Translate the points along their normals if the stroke is outside or inside + if stroke.kind != stroke::StrokeKind::Middle { + path.iter_mut() + .for_each(|p| translate_stroke_point(p, stroke)); + } + // expand the bounding box to include the thickness of the path let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) .expand((stroke.width / 2.0) + feathering); @@ -924,7 +966,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { let connect_with_previous = path_type == PathType::Closed || i1 > 0; - let p1 = &path[i1 as usize]; + let p1 = path[i1 as usize]; let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); @@ -966,7 +1008,7 @@ fn stroke_path( let mut i0 = n - 1; for i1 in 0..n { - let p1 = &path[i1 as usize]; + let p1 = path[i1 as usize]; let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1011,7 +1053,7 @@ fn stroke_path( out.reserve_vertices(4 * n as usize); { - let end = &path[0]; + let end = path[0]; let p = end.pos; let n = end.normal; let back_extrude = n.rot90() * feathering; @@ -1032,7 +1074,7 @@ fn stroke_path( let mut i0 = 0; for i1 in 1..n - 1 { - let point = &path[i1 as usize]; + let point = path[i1 as usize]; let p = point.pos; let n = point.normal; out.colored_vertex(p + n * outer_rad, color_outer); @@ -1060,7 +1102,7 @@ fn stroke_path( { let i1 = n - 1; - let end = &path[i1 as usize]; + let end = path[i1 as usize]; let p = end.pos; let n = end.normal; let back_extrude = -n.rot90() * feathering; @@ -1227,11 +1269,20 @@ impl Tessellator { #[inline(always)] pub fn round_to_pixel(&self, point: f32) -> f32 { - if self.options.round_text_to_pixels { - (point * self.pixels_per_point).round() / self.pixels_per_point - } else { - point - } + (point * self.pixels_per_point).round() / self.pixels_per_point + } + + #[inline(always)] + pub fn round_to_pixel_center(&self, point: f32) -> f32 { + ((point * self.pixels_per_point - 0.5).round() + 0.5) / self.pixels_per_point + } + + #[inline(always)] + pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { + pos2( + self.round_to_pixel_center(pos.x), + self.round_to_pixel_center(pos.y), + ) } /// Tessellate a clipped shape into a list of primitives. @@ -1404,11 +1455,13 @@ impl Tessellator { } } + let path_stroke = PathStroke::from(stroke).outside(); self.scratchpad_path.clear(); self.scratchpad_path.add_circle(center, radius); - self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path - .stroke_closed(self.feathering, &stroke.into(), out); + .fill(self.feathering, fill, &path_stroke, out); + self.scratchpad_path + .stroke_closed(self.feathering, &path_stroke, out); } /// Tessellate a single [`EllipseShape`] into a [`Mesh`]. @@ -1471,11 +1524,13 @@ impl Tessellator { points.push(center + Vec2::new(0.0, -radius.y)); points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y))); + let path_stroke = PathStroke::from(stroke).outside(); self.scratchpad_path.clear(); self.scratchpad_path.add_line_loop(&points); - self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path - .stroke_closed(self.feathering, &stroke.into(), out); + .fill(self.feathering, fill, &path_stroke, out); + self.scratchpad_path + .stroke_closed(self.feathering, &path_stroke, out); } /// Tessellate a single [`Mesh`] into a [`Mesh`]. @@ -1562,7 +1617,8 @@ impl Tessellator { closed, "You asked to fill a path that is not closed. That makes no sense." ); - self.scratchpad_path.fill(self.feathering, *fill, out); + self.scratchpad_path + .fill(self.feathering, *fill, stroke, out); } let typ = if *closed { PathType::Closed @@ -1650,7 +1706,7 @@ impl Tessellator { path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); path.add_line_loop(&self.scratchpad_points); - + let path_stroke = PathStroke::from(stroke).outside(); if uv.is_positive() { // Textured let uv_from_pos = |p: Pos2| { @@ -1662,10 +1718,9 @@ impl Tessellator { path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out); } else { // Untextured - path.fill(self.feathering, fill, out); + path.fill(self.feathering, fill, &path_stroke, out); } - - path.stroke_closed(self.feathering, &stroke.into(), out); + path.stroke_closed(self.feathering, &path_stroke, out); } self.feathering = old_feathering; // restore @@ -1701,12 +1756,16 @@ impl Tessellator { out.vertices.reserve(galley.num_vertices); out.indices.reserve(galley.num_indices); - // The contents of the galley is already snapped to pixel coordinates, + // The contents of the galley are already snapped to pixel coordinates, // but we need to make sure the galley ends up on the start of a physical pixel: - let galley_pos = pos2( - self.round_to_pixel(galley_pos.x), - self.round_to_pixel(galley_pos.y), - ); + let galley_pos = if self.options.round_text_to_pixels { + pos2( + self.round_to_pixel(galley_pos.x), + self.round_to_pixel(galley_pos.y), + ) + } else { + *galley_pos + }; let uv_normalizer = vec2( 1.0 / self.font_tex_size[0] as f32, @@ -1782,13 +1841,12 @@ impl Tessellator { if *underline != Stroke::NONE { self.scratchpad_path.clear(); + self.scratchpad_path.add_line_segment([ + self.round_pos_to_pixel_center(row_rect.left_bottom()), + self.round_pos_to_pixel_center(row_rect.right_bottom()), + ]); self.scratchpad_path - .add_line_segment([row_rect.left_bottom(), row_rect.right_bottom()]); - self.scratchpad_path.stroke_open( - self.feathering, - &PathStroke::from(*underline), - out, - ); + .stroke_open(0.0, &PathStroke::from(*underline), out); } } } @@ -1872,7 +1930,8 @@ impl Tessellator { closed, "You asked to fill a path that is not closed. That makes no sense." ); - self.scratchpad_path.fill(self.feathering, fill, out); + self.scratchpad_path + .fill(self.feathering, fill, stroke, out); } let typ = if closed { PathType::Closed From da04339f5ed75fd299c03b7d0fc3b6405be7b8c5 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 30 Aug 2024 11:22:29 +0200 Subject: [PATCH 3/3] Enable rustdoc `generate-link-to-definition` feature on docs.rs (#5030) You can see this feature in action [here](https://docs.rs/sysinfo/latest/src/sysinfo/common/system.rs.html#46) or on any of dtolnay's crates and many others. I found myself going through your project code recently on docs.rs and I was a bit sad I couldn't have this feature enabled. This should fix it at next release. :) --- crates/ecolor/Cargo.toml | 1 + crates/eframe/Cargo.toml | 1 + crates/egui-wgpu/Cargo.toml | 2 +- crates/egui-winit/Cargo.toml | 2 +- crates/egui/Cargo.toml | 1 + crates/egui_demo_app/Cargo.toml | 1 + crates/egui_demo_lib/Cargo.toml | 1 + crates/egui_extras/Cargo.toml | 1 + crates/egui_glow/Cargo.toml | 2 +- crates/emath/Cargo.toml | 1 + crates/epaint/Cargo.toml | 1 + crates/epaint_default_fonts/Cargo.toml | 1 + 12 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml index 0fc5546453a..c1a10070c03 100644 --- a/crates/ecolor/Cargo.toml +++ b/crates/ecolor/Cargo.toml @@ -21,6 +21,7 @@ workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--generate-link-to-definition"] [lib] diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index b438bdea7cc..1621458e961 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -22,6 +22,7 @@ include = [ [package.metadata.docs.rs] all-features = true targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] +rustdoc-args = ["--generate-link-to-definition"] [lints] workspace = true diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 647016e302b..88b81b0c635 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -28,7 +28,7 @@ workspace = true [package.metadata.docs.rs] all-features = true - +rustdoc-args = ["--generate-link-to-definition"] [features] default = [] diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index 4060442f6dc..4472c70f552 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -18,7 +18,7 @@ workspace = true [package.metadata.docs.rs] all-features = true - +rustdoc-args = ["--generate-link-to-definition"] [features] default = ["clipboard", "links", "wayland", "winit/default", "x11"] diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index a920df2bf46..487a4eac6e9 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--generate-link-to-definition"] [lib] diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 827b14fb42d..b1905356b2e 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--generate-link-to-definition"] [lib] crate-type = ["cdylib", "rlib"] diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 88c2e85557c..cc41e923abe 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -24,6 +24,7 @@ workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--generate-link-to-definition"] [lib] diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index 8d1cb543ab9..b13a518e8d0 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -22,6 +22,7 @@ workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--generate-link-to-definition"] [lib] diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index 3a2ac6777fc..094d537dafc 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -24,7 +24,7 @@ workspace = true [package.metadata.docs.rs] all-features = true - +rustdoc-args = ["--generate-link-to-definition"] [features] default = [] diff --git a/crates/emath/Cargo.toml b/crates/emath/Cargo.toml index f47f281c968..6df0da9af24 100644 --- a/crates/emath/Cargo.toml +++ b/crates/emath/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--generate-link-to-definition"] [lib] diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 88ed8eb4220..7815189dc5a 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -23,6 +23,7 @@ workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--generate-link-to-definition"] [lib] diff --git a/crates/epaint_default_fonts/Cargo.toml b/crates/epaint_default_fonts/Cargo.toml index 9cb05fc0728..8d31507d31f 100644 --- a/crates/epaint_default_fonts/Cargo.toml +++ b/crates/epaint_default_fonts/Cargo.toml @@ -27,3 +27,4 @@ workspace = true [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--generate-link-to-definition"]