Add `element_selection_background` highlight to theme (#32388)

Ben Kunkle and Antonio created

Closes #32354

The issue is that we render selections over the text in the agent panel,
but under the text in editor, so themes that have no alpha for the
selection background color (defaults to 0xff) will just occlude the
selected region. Making the selection render under the text in markdown
would be a significant (and complicated) refactor, as selections can
cross element boundaries (i.e. spanning code block and a header after
the code block).

The solution is to add a new highlight to themes
`element_selection_background` that defaults to the local players
selection background with an alpha of 0.25 (roughly equal to 0x3D which
is the alpha we use for selection backgrounds in default themes) if the
alpha of the local players selection is 1.0. The idea here is to give
theme authors more control over how the selections look outside of
editor, as in the agent panel specifically, the background color is
different, so while an alpha of 0.25 looks acceptable, a different color
would likely be better.

CC: @iamnbutler. Would appreciate your thoughts on this. 

> Note: Before and after using Everforest theme

| Before | After | 
|-------| -----|
| <img width="618" alt="Screenshot 2025-06-09 at 5 23 10 PM"
src="https://github.com/user-attachments/assets/41c7aa02-5b3f-45c6-981c-646ab9e2a1f3"
/> | <img width="618" alt="Screenshot 2025-06-09 at 5 25 03 PM"
src="https://github.com/user-attachments/assets/dfb13ffc-1559-4f01-98f1-a7aea68079b7"
/> |

Clearly, the selection in the after doesn't look _that_ great, but it is
better than the before, and this PR makes the color of the selection
configurable by the theme so that this theme author could make it a
lighter color for better contrast.




Release Notes:

- agent panel: Fixed an issue with some themes where selections inside
the agent panel would occlude the selected text completely

Co-authored-by: Antonio <me@as-cii.com>

Change summary

crates/agent_ui/src/active_thread.rs                                      |  4 
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs |  2 
crates/assistant_tools/src/edit_file_tool.rs                              |  2 
crates/assistant_tools/src/terminal_tool.rs                               |  2 
crates/editor/src/hover_popover.rs                                        |  4 
crates/markdown/examples/markdown.rs                                      |  6 
crates/markdown/examples/markdown_as_child.rs                             |  6 
crates/markdown/src/markdown.rs                                           |  1 
crates/recent_projects/src/ssh_connections.rs                             |  2 
crates/theme/src/default_colors.rs                                        |  2 
crates/theme/src/fallback_themes.rs                                       | 20 
crates/theme/src/schema.rs                                                |  8 
crates/theme/src/styles/colors.rs                                         |  2 
crates/theme/src/theme.rs                                                 | 15 
crates/ui_prompt/src/ui_prompt.rs                                         |  5 
15 files changed, 53 insertions(+), 28 deletions(-)

Detailed changes

crates/agent_ui/src/active_thread.rs 🔗

@@ -204,7 +204,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
     MarkdownStyle {
         base_text_style: text_style.clone(),
         syntax: cx.theme().syntax().clone(),
-        selection_background_color: cx.theme().players().local().selection,
+        selection_background_color: cx.theme().colors().element_selection_background,
         code_block_overflow_x_scroll: true,
         table_overflow_x_scroll: true,
         heading_level_styles: Some(HeadingLevelStyles {
@@ -301,7 +301,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
     MarkdownStyle {
         base_text_style: text_style,
         syntax: cx.theme().syntax().clone(),
-        selection_background_color: cx.theme().players().local().selection,
+        selection_background_color: cx.theme().colors().element_selection_background,
         code_block_overflow_x_scroll: false,
         code_block: StyleRefinement {
             margin: EdgesRefinement::default(),

crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs 🔗

@@ -748,7 +748,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
 
     MarkdownStyle {
         base_text_style: text_style.clone(),
-        selection_background_color: cx.theme().players().local().selection,
+        selection_background_color: colors.element_selection_background,
         link: TextStyleRefinement {
             background_color: Some(colors.editor_foreground.opacity(0.025)),
             underline: Some(UnderlineStyle {

crates/assistant_tools/src/edit_file_tool.rs 🔗

@@ -1065,7 +1065,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 
     MarkdownStyle {
         base_text_style: text_style.clone(),
-        selection_background_color: cx.theme().players().local().selection,
+        selection_background_color: cx.theme().colors().element_selection_background,
         ..Default::default()
     }
 }

crates/assistant_tools/src/terminal_tool.rs 🔗

@@ -691,7 +691,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 
     MarkdownStyle {
         base_text_style: text_style.clone(),
-        selection_background_color: cx.theme().players().local().selection,
+        selection_background_color: cx.theme().colors().element_selection_background,
         ..Default::default()
     }
 }

crates/editor/src/hover_popover.rs 🔗

@@ -648,7 +648,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
             ..Default::default()
         },
         syntax: cx.theme().syntax().clone(),
-        selection_background_color: { cx.theme().players().local().selection },
+        selection_background_color: cx.theme().colors().element_selection_background,
         heading: StyleRefinement::default()
             .font_weight(FontWeight::BOLD)
             .text_base()
@@ -697,7 +697,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
             ..Default::default()
         },
         syntax: cx.theme().syntax().clone(),
-        selection_background_color: { cx.theme().players().local().selection },
+        selection_background_color: cx.theme().colors().element_selection_background,
         height_is_multiple_of_line_height: true,
         heading: StyleRefinement::default()
             .font_weight(FontWeight::BOLD)

crates/markdown/examples/markdown.rs 🔗

@@ -107,11 +107,7 @@ impl Render for MarkdownExample {
                 ..Default::default()
             },
             syntax: cx.theme().syntax().clone(),
-            selection_background_color: {
-                let mut selection = cx.theme().players().local().selection;
-                selection.fade_out(0.7);
-                selection
-            },
+            selection_background_color: cx.theme().colors().element_selection_background,
             ..Default::default()
         };
 

crates/markdown/examples/markdown_as_child.rs 🔗

@@ -91,11 +91,7 @@ impl Render for HelloWorld {
                 ..Default::default()
             },
             syntax: cx.theme().syntax().clone(),
-            selection_background_color: {
-                let mut selection = cx.theme().players().local().selection;
-                selection.fade_out(0.7);
-                selection
-            },
+            selection_background_color: cx.theme().colors().element_selection_background,
             heading: Default::default(),
             ..Default::default()
         };

crates/markdown/src/markdown.rs 🔗

@@ -504,7 +504,6 @@ impl MarkdownElement {
         let selection = self.markdown.read(cx).selection;
         let selection_start = rendered_text.position_for_source_index(selection.start);
         let selection_end = rendered_text.position_for_source_index(selection.end);
-
         if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
             selection_start.zip(selection_end)
         {

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -248,7 +248,7 @@ impl Render for SshPrompt {
         text_style.refine(&refinement);
         let markdown_style = MarkdownStyle {
             base_text_style: text_style,
-            selection_background_color: cx.theme().players().local().selection,
+            selection_background_color: cx.theme().colors().element_selection_background,
             ..Default::default()
         };
 

crates/theme/src/default_colors.rs 🔗

@@ -52,6 +52,7 @@ impl ThemeColors {
             element_active: neutral().light_alpha().step_5(),
             element_selected: neutral().light_alpha().step_5(),
             element_disabled: neutral().light_alpha().step_3(),
+            element_selection_background: blue().light().step_3().alpha(0.25),
             drop_target_background: blue().light_alpha().step_2(),
             ghost_element_background: system.transparent,
             ghost_element_hover: neutral().light_alpha().step_3(),
@@ -174,6 +175,7 @@ impl ThemeColors {
             element_active: neutral().dark_alpha().step_5(),
             element_selected: neutral().dark_alpha().step_5(),
             element_disabled: neutral().dark_alpha().step_3(),
+            element_selection_background: blue().dark().step_3().alpha(0.25),
             drop_target_background: blue().dark_alpha().step_2(),
             ghost_element_background: system.transparent,
             ghost_element_hover: neutral().dark_alpha().step_4(),

crates/theme/src/fallback_themes.rs 🔗

@@ -4,7 +4,8 @@ use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearan
 
 use crate::{
     AccentColors, Appearance, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme,
-    SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles, default_color_scales,
+    SystemColors, Theme, ThemeColors, ThemeColorsRefinement, ThemeFamily, ThemeStyles,
+    default_color_scales,
 };
 
 /// The default theme family for Zed.
@@ -41,6 +42,19 @@ pub(crate) fn apply_status_color_defaults(status: &mut StatusColorsRefinement) {
     }
 }
 
+pub(crate) fn apply_theme_color_defaults(
+    theme_colors: &mut ThemeColorsRefinement,
+    player_colors: &PlayerColors,
+) {
+    if theme_colors.element_selection_background.is_none() {
+        let mut selection = player_colors.local().selection;
+        if selection.a == 1.0 {
+            selection.a = 0.25;
+        }
+        theme_colors.element_selection_background = Some(selection);
+    }
+}
+
 pub(crate) fn zed_default_dark() -> Theme {
     let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.);
     let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
@@ -74,6 +88,7 @@ pub(crate) fn zed_default_dark() -> Theme {
         a: 1.0,
     };
 
+    let player = PlayerColors::dark();
     Theme {
         id: "one_dark".to_string(),
         name: "One Dark".into(),
@@ -97,6 +112,7 @@ pub(crate) fn zed_default_dark() -> Theme {
                 element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
                 element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
                 element_disabled: SystemColors::default().transparent,
+                element_selection_background: player.local().selection.alpha(0.25),
                 drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0),
                 ghost_element_background: SystemColors::default().transparent,
                 ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
@@ -258,7 +274,7 @@ pub(crate) fn zed_default_dark() -> Theme {
                 warning_background: yellow,
                 warning_border: yellow,
             },
-            player: PlayerColors::dark(),
+            player,
             syntax: Arc::new(SyntaxTheme {
                 highlights: vec![
                     ("attribute".into(), purple.into()),

crates/theme/src/schema.rs 🔗

@@ -219,6 +219,10 @@ pub struct ThemeColorsContent {
     #[serde(rename = "element.disabled")]
     pub element_disabled: Option<String>,
 
+    /// Background Color. Used for the background of selections in a UI element.
+    #[serde(rename = "element.selection_background")]
+    pub element_selection_background: Option<String>,
+
     /// Background Color. Used for the area that shows where a dragged element will be dropped.
     #[serde(rename = "drop_target.background")]
     pub drop_target_background: Option<String>,
@@ -726,6 +730,10 @@ impl ThemeColorsContent {
                 .element_disabled
                 .as_ref()
                 .and_then(|color| try_parse_color(color).ok()),
+            element_selection_background: self
+                .element_selection_background
+                .as_ref()
+                .and_then(|color| try_parse_color(color).ok()),
             drop_target_background: self
                 .drop_target_background
                 .as_ref()

crates/theme/src/styles/colors.rs 🔗

@@ -51,6 +51,8 @@ pub struct ThemeColors {
     ///
     /// This could include a selected checkbox, a toggleable button that is toggled on, etc.
     pub element_selected: Hsla,
+    /// Background Color. Used for the background of selections in a UI element.
+    pub element_selection_background: Hsla,
     /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on.
     ///
     /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input.

crates/theme/src/theme.rs 🔗

@@ -35,6 +35,7 @@ use serde::Deserialize;
 use uuid::Uuid;
 
 pub use crate::default_colors::*;
+use crate::fallback_themes::apply_theme_color_defaults;
 pub use crate::font_family_cache::*;
 pub use crate::icon_theme::*;
 pub use crate::icon_theme_schema::*;
@@ -165,12 +166,6 @@ impl ThemeFamily {
             AppearanceContent::Dark => Appearance::Dark,
         };
 
-        let mut refined_theme_colors = match theme.appearance {
-            AppearanceContent::Light => ThemeColors::light(),
-            AppearanceContent::Dark => ThemeColors::dark(),
-        };
-        refined_theme_colors.refine(&theme.style.theme_colors_refinement());
-
         let mut refined_status_colors = match theme.appearance {
             AppearanceContent::Light => StatusColors::light(),
             AppearanceContent::Dark => StatusColors::dark(),
@@ -185,6 +180,14 @@ impl ThemeFamily {
         };
         refined_player_colors.merge(&theme.style.players);
 
+        let mut refined_theme_colors = match theme.appearance {
+            AppearanceContent::Light => ThemeColors::light(),
+            AppearanceContent::Dark => ThemeColors::dark(),
+        };
+        let mut theme_colors_refinement = theme.style.theme_colors_refinement();
+        apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors);
+        refined_theme_colors.refine(&theme_colors_refinement);
+
         let mut refined_accent_colors = match theme.appearance {
             AppearanceContent::Light => AccentColors::light(),
             AppearanceContent::Dark => AccentColors::dark(),

crates/ui_prompt/src/ui_prompt.rs 🔗

@@ -153,7 +153,10 @@ impl Render for ZedPromptRenderer {
                         });
                         MarkdownStyle {
                             base_text_style,
-                            selection_background_color: { cx.theme().players().local().selection },
+                            selection_background_color: cx
+                                .theme()
+                                .colors()
+                                .element_selection_background,
                             ..Default::default()
                         }
                     }))