theme: Add colors for minimap thumb and border (#30785)

A user on Discord reported an issue where the minimap thumb was fully
opaque:

<img
src="https://github.com/user-attachments/assets/5049c6a3-f89a-4ceb-9d1b-ec06e7fe9151"
height="300">

This can happen because the scrollbar and its thumb might not
neccessarily be transparent at all.

Thus, this PR adds the`minimap.thumb.background` and
`minimap.thumb.border` colors to themes so theme authors can specify
custom colors for both here.
Furthermore, I ensured that the minimap thumb background fallback value
can never be entirely opaque. The values were arbitrarily chosen to
avoid the issue from occuring whilst keeping currently working setups
working. With the new properties added, authors (and users) should be
able to avoid running into this issue altogether so I would argue for
this special casing to be fine. However, open to change it should a
different approach be preferrred.

Release Notes:

- Added `minimap.thumb.background` and `minimap.thumb.border` to themes
to customize the thumb color and background of the minimap.
- Fixed an issue where the minimap thumb could be opaque if the theme
did not specify a color for the thumb.
This commit is contained in:
Finn Evers 2025-05-26 20:23:41 +02:00 committed by GitHub
parent 8a24f9f280
commit 4c396bcc91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 194 additions and 67 deletions

View file

@ -1565,11 +1565,13 @@ impl EditorElement {
.map(|vertical_scrollbar| vertical_scrollbar.hitbox.origin)
.unwrap_or_else(|| editor_bounds.top_right());
let thumb_state = self
.editor
.read_with(cx, |editor, _| editor.scroll_manager.minimap_thumb_state());
let show_thumb = match minimap_settings.thumb {
MinimapThumb::Always => true,
MinimapThumb::Hover => self.editor.update(cx, |editor, _| {
editor.scroll_manager.minimap_thumb_visible()
}),
MinimapThumb::Hover => thumb_state.is_some(),
};
let minimap_bounds = Bounds::from_corner_and_size(
@ -1610,7 +1612,8 @@ impl EditorElement {
scroll_position,
minimap_scroll_top,
show_thumb,
);
)
.with_thumb_state(thumb_state);
minimap_editor.update(cx, |editor, cx| {
editor.set_scroll_position(point(0., minimap_scroll_top), window, cx)
@ -5703,10 +5706,7 @@ impl EditorElement {
.get_hovered_axis(window)
.filter(|_| !event.dragging())
{
if layout
.thumb_bounds
.is_some_and(|bounds| bounds.contains(&event.position))
{
if layout.thumb_hovered(&event.position) {
editor
.scroll_manager
.set_hovered_scroll_thumb_axis(axis, cx);
@ -6115,6 +6115,17 @@ impl EditorElement {
window.with_element_namespace("minimap", |window| {
layout.minimap.paint(window, cx);
if let Some(thumb_bounds) = layout.thumb_layout.thumb_bounds {
let minimap_thumb_color = match layout.thumb_layout.thumb_state {
ScrollbarThumbState::Idle => {
cx.theme().colors().minimap_thumb_background
}
ScrollbarThumbState::Hovered => {
cx.theme().colors().minimap_thumb_hover_background
}
ScrollbarThumbState::Dragging => {
cx.theme().colors().minimap_thumb_active_background
}
};
let minimap_thumb_border = match layout.thumb_border_style {
MinimapThumbBorder::Full => Edges::all(ScrollbarLayout::BORDER_WIDTH),
MinimapThumbBorder::LeftOnly => Edges {
@ -6140,9 +6151,9 @@ impl EditorElement {
window.paint_quad(quad(
thumb_bounds,
Corners::default(),
cx.theme().colors().scrollbar_thumb_background,
minimap_thumb_color,
minimap_thumb_border,
cx.theme().colors().scrollbar_thumb_border,
cx.theme().colors().minimap_thumb_border,
BorderStyle::Solid,
));
});
@ -6187,10 +6198,15 @@ impl EditorElement {
}
cx.stop_propagation();
} else {
editor.scroll_manager.set_is_dragging_minimap(false, cx);
if minimap_hitbox.is_hovered(window) {
editor.scroll_manager.show_minimap_thumb(cx);
editor.scroll_manager.set_is_hovering_minimap_thumb(
!event.dragging()
&& layout
.thumb_layout
.thumb_bounds
.is_some_and(|bounds| bounds.contains(&event.position)),
cx,
);
// Stop hover events from propagating to the
// underlying editor if the minimap hitbox is hovered
@ -6209,13 +6225,23 @@ impl EditorElement {
if self.editor.read(cx).scroll_manager.is_dragging_minimap() {
window.on_mouse_event({
let editor = self.editor.clone();
move |_: &MouseUpEvent, phase, _, cx| {
move |event: &MouseUpEvent, phase, window, cx| {
if phase == DispatchPhase::Capture {
return;
}
editor.update(cx, |editor, cx| {
editor.scroll_manager.set_is_dragging_minimap(false, cx);
if minimap_hitbox.is_hovered(window) {
editor.scroll_manager.set_is_hovering_minimap_thumb(
layout
.thumb_layout
.thumb_bounds
.is_some_and(|bounds| bounds.contains(&event.position)),
cx,
);
} else {
editor.scroll_manager.hide_minimap_thumb(cx);
}
cx.stop_propagation();
});
}
@ -6254,7 +6280,7 @@ impl EditorElement {
editor.set_scroll_position(scroll_position, window, cx);
}
editor.scroll_manager.set_is_dragging_minimap(true, cx);
editor.scroll_manager.set_is_dragging_minimap(cx);
cx.stop_propagation();
});
}
@ -8821,10 +8847,6 @@ impl EditorScrollbars {
axis != ScrollbarAxis::Horizontal || viewport_size < scroll_range
})
.map(|(viewport_size, scroll_range)| {
let thumb_state = scrollbar_state
.and_then(|state| state.thumb_state_for_axis(axis))
.unwrap_or(ScrollbarThumbState::Idle);
ScrollbarLayout::new(
window.insert_hitbox(scrollbar_bounds_for(axis), false),
viewport_size,
@ -8833,9 +8855,11 @@ impl EditorScrollbars {
content_offset.along(axis),
scroll_position.along(axis),
show_scrollbars,
thumb_state,
axis,
)
.with_thumb_state(
scrollbar_state.and_then(|state| state.thumb_state_for_axis(axis)),
)
})
};
@ -8885,7 +8909,6 @@ impl ScrollbarLayout {
content_offset: Pixels,
scroll_position: f32,
show_thumb: bool,
thumb_state: ScrollbarThumbState,
axis: ScrollbarAxis,
) -> Self {
let track_bounds = scrollbar_track_hitbox.bounds;
@ -8902,7 +8925,6 @@ impl ScrollbarLayout {
content_offset,
scroll_position,
show_thumb,
thumb_state,
axis,
)
}
@ -8944,7 +8966,6 @@ impl ScrollbarLayout {
track_top_offset,
scroll_position,
show_thumb,
ScrollbarThumbState::Idle,
ScrollbarAxis::Vertical,
)
}
@ -8958,7 +8979,6 @@ impl ScrollbarLayout {
content_offset: Pixels,
scroll_position: f32,
show_thumb: bool,
thumb_state: ScrollbarThumbState,
axis: ScrollbarAxis,
) -> Self {
let text_units_per_page = viewport_size / glyph_space;
@ -8996,7 +9016,18 @@ impl ScrollbarLayout {
visible_range,
text_unit_size,
thumb_bounds,
thumb_state,
thumb_state: Default::default(),
}
}
fn with_thumb_state(self, thumb_state: Option<ScrollbarThumbState>) -> Self {
if let Some(thumb_state) = thumb_state {
Self {
thumb_state,
..self
}
} else {
self
}
}
@ -9017,6 +9048,11 @@ impl ScrollbarLayout {
)
}
fn thumb_hovered(&self, position: &gpui::Point<Pixels>) -> bool {
self.thumb_bounds
.is_some_and(|bounds| bounds.contains(position))
}
fn marker_quads_for_ranges(
&self,
row_ranges: impl IntoIterator<Item = ColoredRange<DisplayRow>>,

View file

@ -123,8 +123,9 @@ impl OngoingScroll {
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[derive(Copy, Clone, Default, PartialEq, Eq)]
pub enum ScrollbarThumbState {
#[default]
Idle,
Hovered,
Dragging,
@ -157,8 +158,7 @@ pub struct ScrollManager {
active_scrollbar: Option<ActiveScrollbarState>,
visible_line_count: Option<f32>,
forbid_vertical_scroll: bool,
dragging_minimap: bool,
show_minimap_thumb: bool,
minimap_thumb_state: Option<ScrollbarThumbState>,
}
impl ScrollManager {
@ -174,8 +174,7 @@ impl ScrollManager {
last_autoscroll: None,
visible_line_count: None,
forbid_vertical_scroll: false,
dragging_minimap: false,
show_minimap_thumb: false,
minimap_thumb_state: None,
}
}
@ -345,24 +344,6 @@ impl ScrollManager {
self.show_scrollbars
}
pub fn show_minimap_thumb(&mut self, cx: &mut Context<Editor>) {
if !self.show_minimap_thumb {
self.show_minimap_thumb = true;
cx.notify();
}
}
pub fn hide_minimap_thumb(&mut self, cx: &mut Context<Editor>) {
if self.show_minimap_thumb {
self.show_minimap_thumb = false;
cx.notify();
}
}
pub fn minimap_thumb_visible(&mut self) -> bool {
self.show_minimap_thumb
}
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
}
@ -419,13 +400,43 @@ impl ScrollManager {
}
}
pub fn is_dragging_minimap(&self) -> bool {
self.dragging_minimap
pub fn set_is_hovering_minimap_thumb(&mut self, hovered: bool, cx: &mut Context<Editor>) {
self.update_minimap_thumb_state(
Some(if hovered {
ScrollbarThumbState::Hovered
} else {
ScrollbarThumbState::Idle
}),
cx,
);
}
pub fn set_is_dragging_minimap(&mut self, dragging: bool, cx: &mut Context<Editor>) {
self.dragging_minimap = dragging;
cx.notify();
pub fn set_is_dragging_minimap(&mut self, cx: &mut Context<Editor>) {
self.update_minimap_thumb_state(Some(ScrollbarThumbState::Dragging), cx);
}
pub fn hide_minimap_thumb(&mut self, cx: &mut Context<Editor>) {
self.update_minimap_thumb_state(None, cx);
}
pub fn is_dragging_minimap(&self) -> bool {
self.minimap_thumb_state
.is_some_and(|state| state == ScrollbarThumbState::Dragging)
}
fn update_minimap_thumb_state(
&mut self,
thumb_state: Option<ScrollbarThumbState>,
cx: &mut Context<Editor>,
) {
if self.minimap_thumb_state != thumb_state {
self.minimap_thumb_state = thumb_state;
cx.notify();
}
}
pub fn minimap_thumb_state(&self) -> Option<ScrollbarThumbState> {
self.minimap_thumb_state
}
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {

View file

@ -90,6 +90,10 @@ impl ThemeColors {
scrollbar_thumb_border: gpui::transparent_black(),
scrollbar_track_background: gpui::transparent_black(),
scrollbar_track_border: neutral().light().step_5(),
minimap_thumb_background: neutral().light_alpha().step_3().alpha(0.7),
minimap_thumb_hover_background: neutral().light_alpha().step_4().alpha(0.7),
minimap_thumb_active_background: neutral().light_alpha().step_5().alpha(0.7),
minimap_thumb_border: gpui::transparent_black(),
editor_foreground: neutral().light().step_12(),
editor_background: neutral().light().step_1(),
editor_gutter_background: neutral().light().step_1(),
@ -211,6 +215,10 @@ impl ThemeColors {
scrollbar_thumb_border: gpui::transparent_black(),
scrollbar_track_background: gpui::transparent_black(),
scrollbar_track_border: neutral().dark().step_5(),
minimap_thumb_background: neutral().dark_alpha().step_3().alpha(0.7),
minimap_thumb_hover_background: neutral().dark_alpha().step_4().alpha(0.7),
minimap_thumb_active_background: neutral().dark_alpha().step_5().alpha(0.7),
minimap_thumb_border: gpui::transparent_black(),
editor_foreground: neutral().dark().step_12(),
editor_background: neutral().dark().step_1(),
editor_gutter_background: neutral().dark().step_1(),

View file

@ -199,6 +199,10 @@ pub(crate) fn zed_default_dark() -> Theme {
scrollbar_thumb_border: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
scrollbar_track_background: gpui::transparent_black(),
scrollbar_track_border: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
minimap_thumb_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 0.7),
minimap_thumb_hover_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 0.7),
minimap_thumb_active_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 0.7),
minimap_thumb_border: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
editor_foreground: hsla(218. / 360., 14. / 100., 71. / 100., 1.),
link_text_hover: blue,
version_control_added: ADDED_COLOR,

View file

@ -28,6 +28,18 @@ pub(crate) fn try_parse_color(color: &str) -> Result<Hsla> {
Ok(hsla)
}
fn ensure_non_opaque(color: Hsla) -> Hsla {
const MAXIMUM_OPACITY: f32 = 0.7;
if color.a <= MAXIMUM_OPACITY {
color
} else {
Hsla {
a: MAXIMUM_OPACITY,
..color
}
}
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AppearanceContent {
@ -374,6 +386,22 @@ pub struct ThemeColorsContent {
#[serde(rename = "scrollbar.track.border")]
pub scrollbar_track_border: Option<String>,
/// The color of the minimap thumb.
#[serde(rename = "minimap.thumb.background")]
pub minimap_thumb_background: Option<String>,
/// The color of the minimap thumb when hovered over.
#[serde(rename = "minimap.thumb.hover_background")]
pub minimap_thumb_hover_background: Option<String>,
/// The color of the minimap thumb whilst being actively dragged.
#[serde(rename = "minimap.thumb.active_background")]
pub minimap_thumb_active_background: Option<String>,
/// The border color of the minimap thumb.
#[serde(rename = "minimap.thumb.border")]
pub minimap_thumb_border: Option<String>,
#[serde(rename = "editor.foreground")]
pub editor_foreground: Option<String>,
@ -635,6 +663,19 @@ impl ThemeColorsContent {
.as_ref()
.and_then(|color| try_parse_color(color).ok())
});
let scrollbar_thumb_hover_background = self
.scrollbar_thumb_hover_background
.as_ref()
.and_then(|color| try_parse_color(color).ok());
let scrollbar_thumb_active_background = self
.scrollbar_thumb_active_background
.as_ref()
.and_then(|color| try_parse_color(color).ok())
.or(scrollbar_thumb_background);
let scrollbar_thumb_border = self
.scrollbar_thumb_border
.as_ref()
.and_then(|color| try_parse_color(color).ok());
ThemeColorsRefinement {
border,
border_variant: self
@ -819,19 +860,9 @@ impl ThemeColorsContent {
.and_then(|color| try_parse_color(color).ok())
.or(border),
scrollbar_thumb_background,
scrollbar_thumb_hover_background: self
.scrollbar_thumb_hover_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
scrollbar_thumb_active_background: self
.scrollbar_thumb_active_background
.as_ref()
.and_then(|color| try_parse_color(color).ok())
.or(scrollbar_thumb_background),
scrollbar_thumb_border: self
.scrollbar_thumb_border
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
scrollbar_thumb_hover_background,
scrollbar_thumb_active_background,
scrollbar_thumb_border,
scrollbar_track_background: self
.scrollbar_track_background
.as_ref()
@ -840,6 +871,26 @@ impl ThemeColorsContent {
.scrollbar_track_border
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
minimap_thumb_background: self
.minimap_thumb_background
.as_ref()
.and_then(|color| try_parse_color(color).ok())
.or(scrollbar_thumb_background.map(ensure_non_opaque)),
minimap_thumb_hover_background: self
.minimap_thumb_hover_background
.as_ref()
.and_then(|color| try_parse_color(color).ok())
.or(scrollbar_thumb_hover_background.map(ensure_non_opaque)),
minimap_thumb_active_background: self
.minimap_thumb_active_background
.as_ref()
.and_then(|color| try_parse_color(color).ok())
.or(scrollbar_thumb_active_background.map(ensure_non_opaque)),
minimap_thumb_border: self
.minimap_thumb_border
.as_ref()
.and_then(|color| try_parse_color(color).ok())
.or(scrollbar_thumb_border),
editor_foreground: self
.editor_foreground
.as_ref()

View file

@ -143,6 +143,14 @@ pub struct ThemeColors {
pub scrollbar_track_background: Hsla,
/// The border color of the scrollbar track.
pub scrollbar_track_border: Hsla,
/// The color of the minimap thumb.
pub minimap_thumb_background: Hsla,
/// The color of the minimap thumb when hovered over.
pub minimap_thumb_hover_background: Hsla,
/// The color of the minimap thumb whilst being actively dragged.
pub minimap_thumb_active_background: Hsla,
/// The border color of the minimap thumb.
pub minimap_thumb_border: Hsla,
// ===
// Editor
@ -327,6 +335,10 @@ pub enum ThemeColorField {
ScrollbarThumbBorder,
ScrollbarTrackBackground,
ScrollbarTrackBorder,
MinimapThumbBackground,
MinimapThumbHoverBackground,
MinimapThumbActiveBackground,
MinimapThumbBorder,
EditorForeground,
EditorBackground,
EditorGutterBackground,
@ -437,6 +449,10 @@ impl ThemeColors {
ThemeColorField::ScrollbarThumbBorder => self.scrollbar_thumb_border,
ThemeColorField::ScrollbarTrackBackground => self.scrollbar_track_background,
ThemeColorField::ScrollbarTrackBorder => self.scrollbar_track_border,
ThemeColorField::MinimapThumbBackground => self.minimap_thumb_background,
ThemeColorField::MinimapThumbHoverBackground => self.minimap_thumb_hover_background,
ThemeColorField::MinimapThumbActiveBackground => self.minimap_thumb_active_background,
ThemeColorField::MinimapThumbBorder => self.minimap_thumb_border,
ThemeColorField::EditorForeground => self.editor_foreground,
ThemeColorField::EditorBackground => self.editor_background,
ThemeColorField::EditorGutterBackground => self.editor_gutter_background,

View file

@ -174,6 +174,7 @@ impl VsCodeThemeConverter {
scrollbar_thumb_border: vscode_scrollbar_slider_background.clone(),
scrollbar_track_background: vscode_editor_background.clone(),
scrollbar_track_border: vscode_colors.editor_overview_ruler.border.clone(),
minimap_thumb_background: vscode_colors.minimap_slider.background.clone(),
editor_foreground: vscode_editor_foreground
.clone()
.or(vscode_token_colors_foreground.clone()),