Skip to content
Open
4 changes: 4 additions & 0 deletions src/doc/rustdoc/src/unstable-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ implemented as a hard-coded list, these traits have a special marker attribute
on them: `#[doc(notable_trait)]`. This means that you can apply this attribute
to your own trait to include it in the "Notable traits" dialog in documentation.

In addition to the "Notable traits" dialog, every type that implements a
`#[doc(notable_trait)]` trait renders a colored badge for that trait at the top
of its page, making the relationship easy to spot when browsing the type.

The `#[doc(notable_trait)]` attribute currently requires the `#![feature(doc_notable_trait)]`
feature gate. For more information, see [its chapter in the Unstable Book][unstable-notable_trait]
and [its tracking issue][issue-notable_trait].
Expand Down
59 changes: 51 additions & 8 deletions src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ mod write_shared;

use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::VecDeque;
use std::collections::{BTreeMap, VecDeque};
use std::fmt::{self, Display as _, Write};
use std::iter::Peekable;
use std::path::PathBuf;
Expand Down Expand Up @@ -1664,6 +1664,15 @@ fn should_render_item(item: &clean::Item, deref_mut_: bool, tcx: TyCtxt<'_>) ->
}
}

/// `Box` has pass-through impls for `Read`, `Write`, `Iterator`, and `Future` when the
/// boxed type implements one of those. We don't want to treat every `Box` return
/// as being notably an `Iterator` (etc), though, so we exempt it. `Pin` has the same
/// issue, with a pass-through impl for `Future`.
fn is_notable_trait_passthrough(did: DefId, cx: &Context<'_>) -> bool {
let lang_items = cx.tcx().lang_items();
Some(did) == lang_items.owned_box() || Some(did) == lang_items.pin_type()
}

fn notable_traits_button(ty: &clean::Type, cx: &Context<'_>) -> Option<impl fmt::Display> {
if ty.is_unit() {
// Very common fast path.
Expand All @@ -1672,13 +1681,7 @@ fn notable_traits_button(ty: &clean::Type, cx: &Context<'_>) -> Option<impl fmt:

let did = ty.def_id(cx.cache())?;

// Box has pass-through impls for Read, Write, Iterator, and Future when the
// boxed type implements one of those. We don't want to treat every Box return
// as being notably an Iterator (etc), though, so we exempt it. Pin has the same
// issue, with a pass-through impl for Future.
if Some(did) == cx.tcx().lang_items().owned_box()
|| Some(did) == cx.tcx().lang_items().pin_type()
{
if is_notable_trait_passthrough(did, cx) {
return None;
}

Expand Down Expand Up @@ -1790,6 +1793,46 @@ fn notable_traits_json<'a>(tys: impl Iterator<Item = &'a clean::Type>, cx: &Cont
serde_json::to_string(&mp).expect("serialize (string, string) -> json object cannot fail")
}

pub(crate) struct NotableTraitBadge {
pub name: String,
pub full_path: String,
/// Relative URL to the trait page, or `None` if it cannot be linked.
pub href: Option<String>,
}

/// Returns all `#[doc(notable_trait)]` traits that `item` implements, to be
/// rendered as badges at the top of the item's page.
pub(crate) fn notable_trait_badges(item: &clean::Item, cx: &Context<'_>) -> Vec<NotableTraitBadge> {
let tcx = cx.tcx();
if let Some(def_id) = item.def_id()
&& !is_notable_trait_passthrough(def_id, cx)
&& let Some(impls) = cx.cache().impls.get(&def_id)
{
impls
.iter()
.map(Impl::inner_impl)
.filter(|impl_| impl_.polarity == ty::ImplPolarity::Positive)
.filter_map(|impl_| {
let path_ = impl_.trait_.as_ref()?;
let trait_did = path_.def_id();
if !cx.cache().traits.get(&trait_did)?.is_notable_trait(tcx) {
return None;
}
let name = tcx.item_name(trait_did).to_string();
let (full_path, href) = match href(trait_did, cx) {
Ok(info) => (join_path_syms(&info.rust_path), Some(info.url)),
Err(_) => (tcx.def_path_str(trait_did), None),
};
Some((name.clone(), NotableTraitBadge { name, full_path, href }))

@ThierryBerger ThierryBerger Jul 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use if let chains too

View changes since the review

})
.collect::<BTreeMap<String, NotableTraitBadge>>()
.into_values()
.collect()
} else {
Vec::new()
}
}

#[derive(Clone, Copy, Debug)]
struct ImplRenderingParameters {
show_def_docs: bool,
Expand Down
34 changes: 33 additions & 1 deletion src/librustdoc/html/render/print_item.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::fmt::{self, Display, Write as _};
use std::hash::{Hash, Hasher};
use std::iter;

use askama::Template;
Expand Down Expand Up @@ -38,7 +40,7 @@ use crate::html::format::{
};
use crate::html::markdown::{HeadingOffset, MarkdownSummaryLine};
use crate::html::render::sidebar::filters;
use crate::html::render::{document_full, document_item_info};
use crate::html::render::{document_full, document_item_info, notable_trait_badges};
use crate::html::url_parts_builder::UrlPartsBuilder;

const ITEM_TABLE_OPEN: &str = "<dl class=\"item-table\">";
Expand All @@ -51,6 +53,15 @@ struct PathComponent {
name: Symbol,
}

struct NotableTraitBadgeVars {
name: String,
full_path: String,
/// Relative URL to the trait page, or `None` when not linkable.
href: Option<String>,

@GuillaumeGomez GuillaumeGomez Jun 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the trait is not linkable, we should not generate a badge for it (I mentioned that here).

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By that I mean: you can put the current colors in the newly created CSS classes. There should be 6 colors from what we wrote in the rustdoc meeting, so 6 different CSS classes.

it does, yes ; we should probably decide what to do about the notable trait visibility on function return type to align behaviors ? I'd say keep it consistent and then unify behavior on a follow up pr ? But no strong feeling, can do here, I just fear controversial discussions on tangantial changes :p

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errata: notable traits may be surfaced when the deps are not built ; minimal impact but we should keep option for local builds then.

/// Index of the `.notable-trait-badge-{n}` color class.
color_index: u8,
}

#[derive(Template)]
#[template(path = "print_item.html")]
struct ItemVars<'a> {
Expand All @@ -59,6 +70,7 @@ struct ItemVars<'a> {
item_type: &'a str,
path_components: Vec<PathComponent>,
stability_since_raw: &'a str,
notable_trait_badges: Vec<NotableTraitBadgeVars>,
src_href: Option<&'a str>,
}

Expand Down Expand Up @@ -112,6 +124,25 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp
let src_href =
if cx.info.include_sources && !item.is_primitive() { cx.src_href(item) } else { None };

let notable_trait_badges: Vec<NotableTraitBadgeVars> = notable_trait_badges(item, cx)
.into_iter()
.map(|info| {
// Stable per-trait color from a hash of the trait path so the
// same trait gets the same badge color across pages.
// This won't be stable between releases though.
let mut h = DefaultHasher::new();
info.full_path.hash(&mut h);
const BADGE_COLORS: u8 = 6;
let color_index = (h.finish() as u8) % BADGE_COLORS;
NotableTraitBadgeVars {
name: info.name,
full_path: info.full_path,
href: info.href,
color_index,
}
})
.collect();

let path_components = if item.is_fake_item() {
vec![]
} else {
Expand All @@ -135,6 +166,7 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp
item_type: &item.type_().to_string(),
path_components,
stability_since_raw: &stability_since_raw,
notable_trait_badges,
src_href: src_href.as_deref(),
};

Expand Down
26 changes: 26 additions & 0 deletions src/librustdoc/html/static/css/rustdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,32 @@ so that we can apply CSS-filters to change the arrow color in themes */
font-size: initial;
}

.notable-trait-badge-container {
padding: 0.5rem 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}

.notable-trait-badge {
display: flex;
align-items: center;
width: fit-content;
height: 1.5rem;
padding: 0 0.5rem;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: normal;
color: white;
}

.notable-trait-badge-0 { background: oklch(0.55 0.21 0); }
.notable-trait-badge-1 { background: oklch(0.55 0.21 60); }
.notable-trait-badge-2 { background: oklch(0.55 0.21 120); }
.notable-trait-badge-3 { background: oklch(0.55 0.21 180); }
.notable-trait-badge-4 { background: oklch(0.55 0.21 240); }
.notable-trait-badge-5 { background: oklch(0.55 0.21 300); }

.rightside {
padding-left: 12px;
float: right;
Expand Down
9 changes: 9 additions & 0 deletions src/librustdoc/html/templates/print_item.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ <h1>
</h1> {# #}
<rustdoc-toolbar></rustdoc-toolbar> {# #}
<span class="sub-heading">
{% if !notable_trait_badges.is_empty() %}
<div class="notable-trait-badge-container">
{% for badge in notable_trait_badges.iter() %}
<a class="notable-trait-badge notable-trait-badge-{{badge.color_index}}"
{% if let Some(href) = badge.href %}href="{{href|safe}}" {% endif %} {#+ #}
title="{{badge.full_path}}">{{badge.name}}</a>
{% endfor %}
</div>
{% endif %}
{% if !stability_since_raw.is_empty() %}
{{ stability_since_raw|safe +}}
{% endif %}
Expand Down
13 changes: 13 additions & 0 deletions tests/rustdoc-html/notable-trait/notable-trait-badge-generic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#![feature(doc_notable_trait)]
#![crate_name = "foo"]

#[doc(notable_trait)]
pub trait Labeled {}

pub trait Bound {}

// A conditional impl: the badge is rendered unconditionally even though the
// impl only holds for `T: Bound`.
//@ has 'foo/struct.Wrapper.html' '//div[@class="notable-trait-badge-container"]/a[@href="trait.Labeled.html"]' 'Labeled'
pub struct Wrapper<T>(pub T);
impl<T: Bound> Labeled for Wrapper<T> {}
10 changes: 10 additions & 0 deletions tests/rustdoc-html/notable-trait/notable-trait-badge-negative.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#![feature(doc_notable_trait, negative_impls)]
#![crate_name = "foo"]

#[doc(notable_trait)]
pub trait Labeled {}

// A negative impl must not produce a badge.
//@ count 'foo/struct.Neg.html' '//div[@class="notable-trait-badge-container"]' 0
pub struct Neg;
impl !Labeled for Neg {}

@ThierryBerger ThierryBerger Jul 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe doc hidden, private traits should be tested too?

And unlinkable trait but not sure we can emulate an unbuilt doc dependency

View changes since the review

16 changes: 16 additions & 0 deletions tests/rustdoc-html/notable-trait/notable-trait-badge-supertrait.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![feature(doc_notable_trait)]
#![crate_name = "foo"]

#[doc(notable_trait)]
pub trait Base {}

pub trait Derived: Base {}

//@ has 'foo/struct.S.html'
// Implementing `Derived` requires implementing the notable supertrait `Base`,
// so its badge shows up.
//@ count - '//div[@class="notable-trait-badge-container"]/a' 1
//@ has - '//div[@class="notable-trait-badge-container"]/a[@href="trait.Base.html"]' 'Base'
pub struct S;
impl Base for S {}
impl Derived for S {}
25 changes: 25 additions & 0 deletions tests/rustdoc-html/notable-trait/notable-trait-badge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#![feature(doc_notable_trait)]
#![crate_name = "foo"]

#[doc(notable_trait)]
pub trait Labeled {}

#[doc(notable_trait)]
pub trait AlsoLabeled {}

pub trait Plain {}

//@ has 'foo/struct.Tagged.html'
//@ has - '//div[@class="notable-trait-badge-container"]/a[@href="trait.Labeled.html"][@title="foo::Labeled"]' 'Labeled'
// Badges are sorted by trait name, so `AlsoLabeled` precedes `Labeled`.
//@ has - '//div[@class="notable-trait-badge-container"]/a[1]' 'AlsoLabeled'
//@ has - '//div[@class="notable-trait-badge-container"]/a[2]' 'Labeled'
pub struct Tagged;
impl Labeled for Tagged {}
impl AlsoLabeled for Tagged {}
impl Plain for Tagged {}

//@ has 'foo/struct.Untagged.html'
//@ count - '//div[@class="notable-trait-badge-container"]' 0
pub struct Untagged;
impl Plain for Untagged {}
Loading