mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-19 11:04:06 +00:00
markdown: Fix code block scrollbar track leaking past rounded corners (#64978)
When hovering a horizontally scrollable code block, the scrollbar track was painted with square corners. Since GPUI content masks are rectangular, the parent's rounded border didn't clip it, so a small corner of the track showed outside the code block's rounded border. This adds `Scrollbars::track_corner_radius` to round the track's outer corners, and markdown code blocks now pass their inner corner radius (corner radius minus border width). Before: <img width="400" alt="CleanShot 2026-09-30 at 3 57 50 PM@2x" src="https://github.com/user-attachments/assets/a90372cf-c668-4469-8dea-058a795888be" /> After: <img width="300" alt="CleanShot 2026-09-30 at 4 00 08 PM@2x" src="https://github.com/user-attachments/assets/cf6c384e-3c93-4a79-9479-d390974c4b67" /> Release Notes: - Fixed a visual artifact at the corners of scrollable code blocks when hovered.
This commit is contained in:
parent
a34a062bb7
commit
da0a7a78cf
2 changed files with 157 additions and 54 deletions
|
|
@ -2819,10 +2819,26 @@ impl Element for MarkdownElement {
|
|||
match (&self.code_block_renderer, is_indented) {
|
||||
(CodeBlockRenderer::Default { .. }, _) | (_, true) => {
|
||||
// This is a parent container that we can position the copy button inside.
|
||||
let parent_container =
|
||||
div().group("code_block").relative().w_full();
|
||||
let mut parent_container = div()
|
||||
.group("code_block")
|
||||
.relative()
|
||||
.w_full()
|
||||
.when(scroll_handle.is_some(), |this| this.rounded_lg());
|
||||
|
||||
let mut parent_container: AnyDiv = if let Some(scroll_handle) =
|
||||
if let CodeBlockRenderer::Default { border: true, .. } =
|
||||
&self.code_block_renderer
|
||||
{
|
||||
parent_container = parent_container
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant);
|
||||
}
|
||||
|
||||
// Styles must be final before `custom_scrollbars`, which reads the
|
||||
// corner radii and borders to round the scrollbar track.
|
||||
parent_container.style().refine(&self.style.code_block);
|
||||
|
||||
let parent_container: AnyDiv = if let Some(scroll_handle) =
|
||||
scroll_handle.as_ref()
|
||||
{
|
||||
let scrollbars = Scrollbars::new(ScrollAxes::Horizontal)
|
||||
|
|
@ -2834,24 +2850,18 @@ impl Element for MarkdownElement {
|
|||
)
|
||||
.notify_content();
|
||||
|
||||
let mut parent_container = parent_container
|
||||
.custom_scrollbars(scrollbars, window, cx);
|
||||
// Keep the code block's padding over the space reserved for the scrollbar track.
|
||||
parent_container
|
||||
.rounded_lg()
|
||||
.custom_scrollbars(scrollbars, window, cx)
|
||||
.into()
|
||||
.style()
|
||||
.padding
|
||||
.refine(&self.style.code_block.padding);
|
||||
parent_container.into()
|
||||
} else {
|
||||
parent_container.into()
|
||||
};
|
||||
|
||||
if let CodeBlockRenderer::Default { border: true, .. } =
|
||||
&self.code_block_renderer
|
||||
{
|
||||
parent_container = parent_container
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant);
|
||||
}
|
||||
|
||||
parent_container.style().refine(&self.style.code_block);
|
||||
builder.push_div(parent_container, range, markdown_end);
|
||||
|
||||
let code_block = div()
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ use gpui::{
|
|||
Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement,
|
||||
IntoElement, IsZero, LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement, Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent,
|
||||
Size, Stateful, StatefulInteractiveElement, Style, Styled, Task, UniformListDecoration,
|
||||
UniformListScrollHandle, Window, ease_in_out, prelude::FluentBuilder as _, px, quad, relative,
|
||||
size,
|
||||
Size, Stateful, StatefulInteractiveElement, Style, StyleRefinement, Styled, Task,
|
||||
UniformListDecoration, UniformListScrollHandle, Window, ease_in_out, fill,
|
||||
prelude::FluentBuilder as _, px, quad, relative, size,
|
||||
};
|
||||
use gpui_util::ResultExt;
|
||||
use smallvec::SmallVec;
|
||||
|
|
@ -17,7 +17,10 @@ use theme::ActiveTheme as _;
|
|||
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::scrollbars::{ScrollbarAutoHide, ScrollbarVisibility, ShowScrollbar};
|
||||
use crate::{
|
||||
scrollbars::{ScrollbarAutoHide, ScrollbarVisibility, ShowScrollbar},
|
||||
utils::inner_corner_radius,
|
||||
};
|
||||
|
||||
const SCROLLBAR_HIDE_DELAY_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const SCROLLBAR_HIDE_DURATION: Duration = Duration::from_millis(400);
|
||||
|
|
@ -68,6 +71,7 @@ pub mod scrollbars {
|
|||
|
||||
fn get_scrollbar_state<T>(
|
||||
mut config: Scrollbars<T>,
|
||||
parent_style: &StyleRefinement,
|
||||
caller_location: &'static std::panic::Location,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
|
|
@ -77,17 +81,21 @@ where
|
|||
{
|
||||
let element_id = config.id.take().unwrap_or_else(|| caller_location.into());
|
||||
let track_color = config.track_color;
|
||||
let track_corner_radii = inner_corner_radii(parent_style, window.rem_size());
|
||||
let has_border = config.border;
|
||||
let reveal_policy = config.reveal_policy;
|
||||
|
||||
let state = window.use_keyed_state(element_id, cx, |_, cx| {
|
||||
let parent_id = cx.entity_id();
|
||||
ScrollbarStateWrapper(cx.new(|cx| ScrollbarState::new_from_config(config, parent_id, cx)))
|
||||
ScrollbarStateWrapper(
|
||||
cx.new(|cx| ScrollbarState::new_from_config(config, track_corner_radii, parent_id, cx)),
|
||||
)
|
||||
});
|
||||
|
||||
state.update(cx, |state, cx| {
|
||||
state.0.update(cx, |state, _cx| {
|
||||
state.update_colors(track_color, has_border);
|
||||
state.track_corner_radii = track_corner_radii;
|
||||
state.reveal_policy = reveal_policy;
|
||||
})
|
||||
});
|
||||
|
|
@ -97,6 +105,8 @@ where
|
|||
pub trait WithScrollbar: Sized {
|
||||
type Output;
|
||||
|
||||
/// The scrollbar tracks are rounded to fit the element's corner radii and borders,
|
||||
/// which are read when this is called, so set them beforehand.
|
||||
fn custom_scrollbars<T>(
|
||||
self,
|
||||
config: Scrollbars<T>,
|
||||
|
|
@ -147,7 +157,7 @@ impl WithScrollbar for Stateful<Div> {
|
|||
|
||||
#[track_caller]
|
||||
fn custom_scrollbars<T>(
|
||||
self,
|
||||
mut self,
|
||||
config: Scrollbars<T>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
|
|
@ -155,11 +165,14 @@ impl WithScrollbar for Stateful<Div> {
|
|||
where
|
||||
T: ScrollableHandle,
|
||||
{
|
||||
render_scrollbar(
|
||||
get_scrollbar_state(config, std::panic::Location::caller(), window, cx),
|
||||
self,
|
||||
let scrollbar = get_scrollbar_state(
|
||||
config,
|
||||
self.style(),
|
||||
std::panic::Location::caller(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
);
|
||||
render_scrollbar(scrollbar, self, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +181,7 @@ impl WithScrollbar for Div {
|
|||
|
||||
#[track_caller]
|
||||
fn custom_scrollbars<T>(
|
||||
self,
|
||||
mut self,
|
||||
config: Scrollbars<T>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
|
|
@ -176,7 +189,13 @@ impl WithScrollbar for Div {
|
|||
where
|
||||
T: ScrollableHandle,
|
||||
{
|
||||
let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
|
||||
let scrollbar = get_scrollbar_state(
|
||||
config,
|
||||
self.style(),
|
||||
std::panic::Location::caller(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
// We know this ID stays consistent as long as the element is rendered for
|
||||
// consecutive frames, which is sufficient for our use case here
|
||||
let scrollbar_entity_id = scrollbar.entity_id();
|
||||
|
|
@ -189,6 +208,33 @@ impl WithScrollbar for Div {
|
|||
}
|
||||
}
|
||||
|
||||
/// Content masks are rectangular, so the parent's rounded corners don't clip the track.
|
||||
/// Rounding the track by the parent's inner radii keeps it from showing past the parent's border.
|
||||
fn inner_corner_radii(style: &StyleRefinement, rem_size: Pixels) -> Corners<Pixels> {
|
||||
let to_pixels =
|
||||
|length: Option<gpui::AbsoluteLength>| length.unwrap_or_default().to_pixels(rem_size);
|
||||
let radii = &style.corner_radii;
|
||||
let borders = &style.border_widths;
|
||||
let top = to_pixels(borders.top);
|
||||
let right = to_pixels(borders.right);
|
||||
let bottom = to_pixels(borders.bottom);
|
||||
let left = to_pixels(borders.left);
|
||||
let inner = |radius, border_a: Pixels, border_b: Pixels| {
|
||||
inner_corner_radius(
|
||||
to_pixels(radius),
|
||||
border_a.max(border_b),
|
||||
Pixels::ZERO,
|
||||
Pixels::ZERO,
|
||||
)
|
||||
};
|
||||
Corners {
|
||||
top_left: inner(radii.top_left, top, left),
|
||||
top_right: inner(radii.top_right, top, right),
|
||||
bottom_right: inner(radii.bottom_right, bottom, right),
|
||||
bottom_left: inner(radii.bottom_left, bottom, left),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_scrollbar<T>(
|
||||
scrollbar: Entity<ScrollbarStateWrapper<T>>,
|
||||
div: Stateful<Div>,
|
||||
|
|
@ -649,6 +695,7 @@ struct ScrollbarState<T: ScrollableHandle = ScrollHandle> {
|
|||
get_visibility: fn(&App) -> ShowScrollbar,
|
||||
visibility: Point<ReservedSpace>,
|
||||
track_color: Option<TrackColors>,
|
||||
track_corner_radii: Corners<Pixels>,
|
||||
reveal_policy: ScrollbarRevealPolicy,
|
||||
show_state: VisibilityState,
|
||||
style: ScrollbarStyle,
|
||||
|
|
@ -658,7 +705,12 @@ struct ScrollbarState<T: ScrollableHandle = ScrollHandle> {
|
|||
}
|
||||
|
||||
impl<T: ScrollableHandle> ScrollbarState<T> {
|
||||
fn new_from_config(config: Scrollbars<T>, parent_id: EntityId, cx: &mut Context<Self>) -> Self {
|
||||
fn new_from_config(
|
||||
config: Scrollbars<T>,
|
||||
track_corner_radii: Corners<Pixels>,
|
||||
parent_id: EntityId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let (manually_added, scroll_handle) = match config.scrollable_handle {
|
||||
Handle::Tracked(handle) => (true, handle),
|
||||
Handle::Untracked(func) => (false, func()),
|
||||
|
|
@ -675,6 +727,7 @@ impl<T: ScrollableHandle> ScrollbarState<T> {
|
|||
background: color,
|
||||
has_border: config.border,
|
||||
}),
|
||||
track_corner_radii,
|
||||
show_behavior,
|
||||
get_visibility: config.get_visibility,
|
||||
style: config.style.unwrap_or_default(),
|
||||
|
|
@ -1421,6 +1474,7 @@ impl<T: ScrollableHandle> Element for ScrollbarElement<T> {
|
|||
let state = self.state.read(cx);
|
||||
let thumb_state = &state.thumb_state;
|
||||
let style = state.style;
|
||||
let track_corner_radii = state.track_corner_radii;
|
||||
|
||||
if thumb_state.is_dragging() {
|
||||
capture_phase = DispatchPhase::Capture;
|
||||
|
|
@ -1475,33 +1529,72 @@ impl<T: ScrollableHandle> Element for ScrollbarElement<T> {
|
|||
track_color.fade_out(fade);
|
||||
}
|
||||
|
||||
let border_edges = has_border
|
||||
.then(|| match axis {
|
||||
ScrollbarAxis::Horizontal => Edges {
|
||||
top: BORDER_WIDTH,
|
||||
..Default::default()
|
||||
},
|
||||
ScrollbarAxis::Vertical => Edges {
|
||||
left: BORDER_WIDTH,
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let border_color = if has_border {
|
||||
cx.theme().colors().border_variant.opacity(0.6)
|
||||
} else {
|
||||
Hsla::transparent_black()
|
||||
let track_corners = match axis {
|
||||
ScrollbarAxis::Horizontal => Corners {
|
||||
bottom_left: track_corner_radii.bottom_left,
|
||||
bottom_right: track_corner_radii.bottom_right,
|
||||
..Default::default()
|
||||
},
|
||||
ScrollbarAxis::Vertical => Corners {
|
||||
top_right: track_corner_radii.top_right,
|
||||
bottom_right: track_corner_radii.bottom_right,
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
window.paint_quad(quad(
|
||||
*track_bounds,
|
||||
Corners::default(),
|
||||
track_color,
|
||||
border_edges,
|
||||
border_color,
|
||||
BorderStyle::Solid,
|
||||
));
|
||||
// Quad radii are clamped to half the quad's size, so a thin track
|
||||
// can't follow a larger parent curve. Paint a quad thick enough
|
||||
// to hold the full radii and crop it back to the track.
|
||||
let largest_radius = track_corners
|
||||
.top_left
|
||||
.max(track_corners.top_right)
|
||||
.max(track_corners.bottom_right)
|
||||
.max(track_corners.bottom_left);
|
||||
let extension = (largest_radius * 2.
|
||||
- track_bounds.size.along(axis.invert()))
|
||||
.max(Pixels::ZERO);
|
||||
let painted_bounds = track_bounds.extend(match axis {
|
||||
ScrollbarAxis::Horizontal => Edges {
|
||||
top: extension,
|
||||
..Default::default()
|
||||
},
|
||||
ScrollbarAxis::Vertical => Edges {
|
||||
left: extension,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
window.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: *track_bounds,
|
||||
}),
|
||||
|window| {
|
||||
window.paint_quad(fill(painted_bounds, track_color).corner_radii(
|
||||
track_corners.clamp_radii_for_quad_size(painted_bounds.size),
|
||||
));
|
||||
},
|
||||
);
|
||||
|
||||
// Painted separately because the crop would remove a border on the extended quad.
|
||||
if has_border {
|
||||
window.paint_quad(quad(
|
||||
*track_bounds,
|
||||
Corners::default(),
|
||||
Hsla::transparent_black(),
|
||||
match axis {
|
||||
ScrollbarAxis::Horizontal => Edges {
|
||||
top: BORDER_WIDTH,
|
||||
..Default::default()
|
||||
},
|
||||
ScrollbarAxis::Vertical => Edges {
|
||||
left: BORDER_WIDTH,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
cx.theme().colors().border_variant.opacity(0.6),
|
||||
BorderStyle::Solid,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
window.paint_quad(quad(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue