git_ui: Color blame timestamps by their age

Lukas Wirth created

Change summary

crates/git_ui/src/blame_ui.rs                 | 37 ++++++++++++++------
crates/settings/src/settings_content/theme.rs |  8 ++++
crates/theme/src/default_colors.rs            |  4 ++
crates/theme/src/fallback_themes.rs           |  2 +
crates/theme/src/schema.rs                    |  8 ++++
crates/theme/src/styles/colors.rs             |  5 ++
6 files changed, 53 insertions(+), 11 deletions(-)

Detailed changes

crates/git_ui/src/blame_ui.rs 🔗

@@ -15,7 +15,7 @@ use markdown::{Markdown, MarkdownElement};
 use project::{git_store::Repository, project_settings::ProjectSettings};
 use settings::Settings as _;
 use theme::ThemeSettings;
-use time::OffsetDateTime;
+use time::{OffsetDateTime, ext::NumericalDuration};
 use time_format::format_local_timestamp;
 use ui::{ContextMenu, Divider, IconButtonShape, prelude::*, tooltip_container};
 use workspace::Workspace;
@@ -42,7 +42,7 @@ impl BlameRenderer for GitBlameRenderer {
         window: &mut Window,
         cx: &mut App,
     ) -> Option<AnyElement> {
-        let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
+        let (time_color, relative_timestamp) = blame_entry_relative_timestamp(cx, &blame_entry);
         let short_commit_id = blame_entry.sha.display_short();
         let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
         let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
@@ -80,7 +80,7 @@ impl BlameRenderer for GitBlameRenderer {
                                 .children(avatar)
                                 .child(name),
                         )
-                        .child(relative_timestamp)
+                        .child(div().text_color(time_color).child(relative_timestamp))
                         .hover(|style| style.bg(cx.theme().colors().element_hover))
                         .cursor_pointer()
                         .on_mouse_down(MouseButton::Right, {
@@ -150,7 +150,7 @@ impl BlameRenderer for GitBlameRenderer {
         blame_entry: BlameEntry,
         cx: &mut App,
     ) -> Option<AnyElement> {
-        let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
+        let (_, relative_timestamp) = blame_entry_relative_timestamp(cx, &blame_entry);
         let author = blame_entry.author.as_deref().unwrap_or_default();
         let summary_enabled = ProjectSettings::get_global(cx)
             .git
@@ -415,17 +415,32 @@ fn deploy_blame_entry_context_menu(
     });
 }
 
-fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String {
+fn blame_entry_relative_timestamp(cx: &App, blame_entry: &BlameEntry) -> (Hsla, String) {
     match blame_entry.author_offset_date_time() {
         Ok(timestamp) => {
+            let mut color = cx.theme().colors().version_control_blame_age_new;
             let local = chrono::Local::now().offset().local_minus_utc();
-            time_format::format_localized_timestamp(
-                timestamp,
-                time::OffsetDateTime::now_utc(),
-                time::UtcOffset::from_whole_seconds(local).unwrap(),
-                time_format::TimestampFormat::Relative,
+            let now = time::OffsetDateTime::now_utc();
+
+            // Gradient over ~2 years
+            let recency = ((now - timestamp - 2.hours()).as_seconds_f32()
+                / 106.weeks().as_seconds_f32())
+            .clamp(0.0, 1.0);
+            // Sqrt to make more recent colors change more strongly
+            color.l *= 1. - 0.45 * recency.sqrt();
+            (
+                color,
+                time_format::format_localized_timestamp(
+                    timestamp,
+                    now,
+                    time::UtcOffset::from_whole_seconds(local).unwrap(),
+                    time_format::TimestampFormat::Relative,
+                ),
             )
         }
-        Err(_) => "Error parsing date".to_string(),
+        Err(_) => (
+            cx.theme().colors().terminal_ansi_bright_red,
+            "Error parsing date".to_string(),
+        ),
     }
 }

crates/settings/src/settings_content/theme.rs 🔗

@@ -777,6 +777,14 @@ pub struct ThemeColorsContent {
     /// Deprecated in favor of `version_control_conflict_marker_theirs`.
     #[deprecated]
     pub version_control_conflict_theirs_background: Option<String>,
+
+    /// Blame gutter color for new revisions.
+    #[serde(rename = "version_control.blame.age_new")]
+    pub version_control_blame_age_new: Option<String>,
+
+    /// Blame gutter color for old revisions.
+    #[serde(rename = "version_control.blame.age_old")]
+    pub version_control_blame_age_old: Option<String>,
 }
 
 #[skip_serializing_none]

crates/theme/src/default_colors.rs 🔗

@@ -154,6 +154,8 @@ impl ThemeColors {
             version_control_ignored: gray().light().step_12(),
             version_control_conflict_marker_ours: green().light().step_10().alpha(0.5),
             version_control_conflict_marker_theirs: blue().light().step_10().alpha(0.5),
+            version_control_blame_age_new: orange().light().step_10(),
+            version_control_blame_age_old: blue().light().step_10(),
         }
     }
 
@@ -280,6 +282,8 @@ impl ThemeColors {
             version_control_ignored: gray().dark().step_12(),
             version_control_conflict_marker_ours: green().dark().step_10().alpha(0.5),
             version_control_conflict_marker_theirs: blue().dark().step_10().alpha(0.5),
+            version_control_blame_age_new: orange().dark().step_10(),
+            version_control_blame_age_old: blue().dark().step_10(),
         }
     }
 }

crates/theme/src/fallback_themes.rs 🔗

@@ -233,6 +233,8 @@ pub(crate) fn zed_default_dark() -> Theme {
                 version_control_ignored: crate::gray().light().step_12(),
                 version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5),
                 version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5),
+                version_control_blame_age_new: crate::orange().dark().step_9(),
+                version_control_blame_age_old: crate::blue().dark().step_9(),
             },
             status: StatusColors {
                 conflict: yellow,

crates/theme/src/schema.rs 🔗

@@ -756,6 +756,14 @@ pub fn theme_colors_refinement(
             .as_ref()
             .or(this.version_control_conflict_theirs_background.as_ref())
             .and_then(|color| try_parse_color(color).ok()),
+        version_control_blame_age_new: this
+            .version_control_blame_age_new
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        version_control_blame_age_old: this
+            .version_control_blame_age_old
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
     }
 }
 

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

@@ -286,6 +286,11 @@ pub struct ThemeColors {
     pub version_control_conflict_marker_ours: Hsla,
     /// Represents the "theirs" region of a merge conflict.
     pub version_control_conflict_marker_theirs: Hsla,
+
+    /// Blame gutter color for new revisions.
+    pub version_control_blame_age_new: Hsla,
+    /// Blame gutter color for old revisions.
+    pub version_control_blame_age_old: Hsla,
 }
 
 #[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]