1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Fix where Panel puts its separator line, and how much room it reserves (#8382)

Two fixes to the separator line of `Panel` (`resolve_frame` was added in
#8367):

* **Reserve room only when the line is always drawn.** Before,
`show_separator_line || resizable` reserved the line's thickness, so a
resizable panel that opted out still got a permanently visible gap along
its inner edge — space held for a line only drawn transiently, while
hovering or dragging the resize handle.
* **Paint the line outside the frame's outline**, in room reserved in
`Frame::outer_margin` rather than `inner_margin`, so going outwards from
the panel contents you get:

  `contents | inner_margin | stroke | separator line | outer_margin`

Previously the line landed on top of the frame's outline (or outside its
outer margin). Default panels — no stroke, no outer margin — are
unchanged pixel-wise.

Found in the Rerun viewer: the time panel is
`.resizable(true).show_separator_line(false)` and draws its own top
line, so the extra 1pt landed above the top bar's buttons, making them
look 1pt too low.

Tests in `tests/egui_tests/tests/test_panel_separator_line.rs`, both
spanning `show_separator_line` on/off × resize handle hovered/not:
snapshots of a top panel with a garish outline, plus a pixel probe
across the inner edge of a panel on each of the four sides. Both fail on
`main`.

* [x] I have followed the instructions in the PR template

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Emil Ernerfeldt
2026-08-03 03:32:56 -07:00
committed by GitHub
parent dcd0c72d53
commit ddec5f3e4c
6 changed files with 166 additions and 13 deletions

View File

@@ -18,8 +18,8 @@
use emath::GuiRounding as _;
use crate::{
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect,
Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, Margin, NumExt as _, Rangef,
Rect, Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
};
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
@@ -126,6 +126,22 @@ impl PanelSide {
}
}
/// The component of `margin` on the panel's _resizable_ edge,
/// i.e. the edge facing the rest of the ui, where the separator line goes.
fn resize_margin(self, mut margin: Margin) -> i8 {
*self.resize_margin_mut(&mut margin)
}
/// Mutable version of [`Self::resize_margin`].
fn resize_margin_mut(self, margin: &mut Margin) -> &mut i8 {
match self {
Self::Left => &mut margin.right,
Self::Right => &mut margin.left,
Self::Top => &mut margin.bottom,
Self::Bottom => &mut margin.top,
}
}
/// Resize by keeping `self` side fixed, and moving the opposite side.
fn set_rect_size(self, rect: &mut Rect, size: f32) {
match self {
@@ -298,6 +314,19 @@ impl Panel {
/// Show a separator line, even when not interacting with it?
///
/// The separator line sits on the panel's inner edge, i.e. the edge facing the rest of the ui.
/// It is painted _outside_ the [`Frame`]'s outline, in room the panel reserves for it in the
/// frame's [`Frame::outer_margin`], so that going from the panel contents outwards you get:
///
/// contents | [`Frame::inner_margin`] | [`Frame::stroke`] | separator line | [`Frame::outer_margin`]
///
/// Turning this off removes that reserved room too, so the panel gets no permanent gap along
/// that edge.
///
/// A `resizable` panel still shows a line while hovered or dragged, regardless of this setting.
/// With this setting off there is no room reserved for it, so that transient line is painted
/// just outside the frame's outline, overlapping the [`Frame::outer_margin`].
///
/// Default: `true`.
#[inline]
pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
@@ -845,7 +874,14 @@ impl Panel {
Stroke::NONE
};
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
let line_pos = side.resize_pos(shifted_outer_rect) + 0.5 * side.sign() * stroke.width;
// The line goes just _outside_ the frame's outline, in the room `resolve_frame`
// reserved for it in the outer margin, i.e.:
//
// contents | `inner_margin` | outline | separator line | `outer_margin`
let outer_margin = f32::from(side.resize_margin(frame.outer_margin));
let outline_edge = side.resize_pos(shifted_outer_rect) + side.sign() * outer_margin;
let line_pos = outline_edge - 0.5 * side.sign() * stroke.width;
let cross_range = shifted_outer_rect.range_along(side.cross_axis());
if axis == 0 {
parent_ui.painter().vline(line_pos, cross_range, stroke);
@@ -863,19 +899,19 @@ impl Panel {
.frame
.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let has_separator_line = self.show_separator_line || self.resizable;
if has_separator_line {
// The separator line has a thickness that we need to account for.
if self.show_separator_line {
// Reserve room for the separator line in the frame's _outer_ margin, so the line
// lands just outside the frame's outline instead of painting on top of it:
//
// contents | `inner_margin` | outline | separator line | `outer_margin`
//
// We deliberately don't do this for a `resizable` panel that has opted out of the
// separator line: the line it shows while hovered/dragged is a transient affordance,
// and reserving room for it would leave a permanently visible gap.
let widgets = &ui.style().visuals.widgets;
let stroke_width = widgets.noninteractive.bg_stroke.width.round() as i8;
let margin_side = match self.side {
PanelSide::Left => &mut frame.inner_margin.right,
PanelSide::Right => &mut frame.inner_margin.left,
PanelSide::Top => &mut frame.inner_margin.bottom,
PanelSide::Bottom => &mut frame.inner_margin.top,
};
let margin_side = self.side.resize_margin_mut(&mut frame.outer_margin);
*margin_side = (*margin_side).saturating_add(stroke_width);
}