diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 6e6fb4cf509..e29d6c2bd23 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -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() diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 8beb4a26ec4..d20a30e8cdc 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -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( mut config: Scrollbars, + 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( self, config: Scrollbars, @@ -147,7 +157,7 @@ impl WithScrollbar for Stateful
{ #[track_caller] fn custom_scrollbars( - self, + mut self, config: Scrollbars, window: &mut Window, cx: &mut App, @@ -155,11 +165,14 @@ impl WithScrollbar for Stateful
{ 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( - self, + mut self, config: Scrollbars, 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 { + let to_pixels = + |length: Option| 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( scrollbar: Entity>, div: Stateful
, @@ -649,6 +695,7 @@ struct ScrollbarState { get_visibility: fn(&App) -> ShowScrollbar, visibility: Point, track_color: Option, + track_corner_radii: Corners, reveal_policy: ScrollbarRevealPolicy, show_state: VisibilityState, style: ScrollbarStyle, @@ -658,7 +705,12 @@ struct ScrollbarState { } impl ScrollbarState { - fn new_from_config(config: Scrollbars, parent_id: EntityId, cx: &mut Context) -> Self { + fn new_from_config( + config: Scrollbars, + track_corner_radii: Corners, + parent_id: EntityId, + cx: &mut Context, + ) -> 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 ScrollbarState { 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 Element for ScrollbarElement { 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 Element for ScrollbarElement { 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(