From 4c396bcc91656c8a684064eaf11e32d63d946e6e Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 26 May 2025 20:23:41 +0200 Subject: [PATCH] theme: Add colors for minimap thumb and border (#30785) A user on Discord reported an issue where the minimap thumb was fully opaque: 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. --- crates/editor/src/element.rs | 88 +++++++++++++------ crates/editor/src/scroll.rs | 65 ++++++++------ crates/theme/src/default_colors.rs | 8 ++ crates/theme/src/fallback_themes.rs | 4 + crates/theme/src/schema.rs | 77 +++++++++++++--- crates/theme/src/styles/colors.rs | 16 ++++ crates/theme_importer/src/vscode/converter.rs | 1 + 7 files changed, 193 insertions(+), 66 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ecddfc24b4595e8dc309300bdee526dd048a7d0e..bdb503ab6a762b6c447bf143a433cd60a544d0a0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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) -> 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) -> bool { + self.thumb_bounds + .is_some_and(|bounds| bounds.contains(position)) + } + fn marker_quads_for_ranges( &self, row_ranges: impl IntoIterator>, diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index e03ee55e6169fb83b35a91387290c79877b27851..a8081b95bde9f52e07dd98d109d46d72971f6165 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -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, visible_line_count: Option, forbid_vertical_scroll: bool, - dragging_minimap: bool, - show_minimap_thumb: bool, + minimap_thumb_state: Option, } 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) { - if !self.show_minimap_thumb { - self.show_minimap_thumb = true; - cx.notify(); - } - } - - pub fn hide_minimap_thumb(&mut self, cx: &mut Context) { - 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 { self.autoscroll_request.map(|(autoscroll, _)| autoscroll) } @@ -419,13 +400,43 @@ impl ScrollManager { } } + pub fn set_is_hovering_minimap_thumb(&mut self, hovered: bool, cx: &mut Context) { + self.update_minimap_thumb_state( + Some(if hovered { + ScrollbarThumbState::Hovered + } else { + ScrollbarThumbState::Idle + }), + cx, + ); + } + + pub fn set_is_dragging_minimap(&mut self, cx: &mut Context) { + self.update_minimap_thumb_state(Some(ScrollbarThumbState::Dragging), cx); + } + + pub fn hide_minimap_thumb(&mut self, cx: &mut Context) { + self.update_minimap_thumb_state(None, cx); + } + pub fn is_dragging_minimap(&self) -> bool { - self.dragging_minimap + self.minimap_thumb_state + .is_some_and(|state| state == ScrollbarThumbState::Dragging) } - pub fn set_is_dragging_minimap(&mut self, dragging: bool, cx: &mut Context) { - self.dragging_minimap = dragging; - cx.notify(); + fn update_minimap_thumb_state( + &mut self, + thumb_state: Option, + cx: &mut Context, + ) { + if self.minimap_thumb_state != thumb_state { + self.minimap_thumb_state = thumb_state; + cx.notify(); + } + } + + pub fn minimap_thumb_state(&self) -> Option { + self.minimap_thumb_state } pub fn clamp_scroll_left(&mut self, max: f32) -> bool { diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 1af59c6776b2807127eae91de9dde6ecd4f49a8a..cc4ad01e4d936f46ce19de77a8aaf5012545c7c1 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -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(), diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index d907da645b55c68ccd425baacbf7832fc615ba45..941a1901bb31784c82c2db1edec0d3a5690fd5ad 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -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, diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index 242091d40a01ffe64ff76dc9dbf901c2d13042e0..32810c2ae7c761b8f5e71ca5306071c060aae137 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -28,6 +28,18 @@ pub(crate) fn try_parse_color(color: &str) -> Result { 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, + /// The color of the minimap thumb. + #[serde(rename = "minimap.thumb.background")] + pub minimap_thumb_background: Option, + + /// The color of the minimap thumb when hovered over. + #[serde(rename = "minimap.thumb.hover_background")] + pub minimap_thumb_hover_background: Option, + + /// The color of the minimap thumb whilst being actively dragged. + #[serde(rename = "minimap.thumb.active_background")] + pub minimap_thumb_active_background: Option, + + /// The border color of the minimap thumb. + #[serde(rename = "minimap.thumb.border")] + pub minimap_thumb_border: Option, + #[serde(rename = "editor.foreground")] pub editor_foreground: Option, @@ -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() diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 3d0df27985269c534ac80a8bc38d056d97fddb42..a66f2067440d92b10515f43489e3183a7e6e5c83 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -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, diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index 99f762589690834f006e27639b5d004adba7bb79..9a17a4cdd2b13e116b81c86c753ccab83a965c79 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -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()),