Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
62 changes: 54 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,49 @@ 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_| {
if let Some(trait_) = &impl_.trait_
&& let trait_did = trait_.def_id()
&& let Some(trait_) = cx.cache().traits.get(&trait_did)
&& trait_.is_notable_trait(tcx)
{
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 }))
} else {
None
}
})
.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
Contributor 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
Contributor 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
12 changes: 12 additions & 0 deletions src/librustdoc/html/static/css/noscript.css
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ nav.sub {
--scrape-example-code-wrapper-background-end: rgba(255, 255, 255, 0);
--sidebar-resizer-hover: hsl(207, 90%, 66%);
--sidebar-resizer-active: hsl(207, 90%, 54%);
--notable-badge-pink: oklch(0.88 0.21 0);
--notable-badge-red: oklch(0.88 0.21 40);
--notable-badge-orange: oklch(0.88 0.21 70);
--notable-badge-green: oklch(0.88 0.21 150);
--notable-badge-blue: oklch(0.88 0.21 240);
--notable-badge-violet: oklch(0.88 0.21 300);
}
/* End theme: light */

Expand Down Expand Up @@ -252,6 +258,12 @@ nav.sub {
--scrape-example-code-wrapper-background-end: rgba(53, 53, 53, 0);
--sidebar-resizer-hover: hsl(207, 30%, 54%);
--sidebar-resizer-active: hsl(207, 90%, 54%);
--notable-badge-pink: oklch(0.55 0.21 0);
--notable-badge-red: oklch(0.55 0.21 40);
--notable-badge-orange: oklch(0.55 0.21 70);
--notable-badge-green: oklch(0.55 0.21 150);
--notable-badge-blue: oklch(0.55 0.21 240);
--notable-badge-violet: oklch(0.55 0.21 300);
}
/* End theme: dark */
}
48 changes: 48 additions & 0 deletions src/librustdoc/html/static/css/rustdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,36 @@ 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-container > a {
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: var(--main-color);
}

.notable-trait-badge-container > a:hover {
text-decoration: none;
}

.notable-trait-badge-container > .badge-0 { background: var(--notable-badge-pink); }
.notable-trait-badge-container > .badge-1 { background: var(--notable-badge-red); }
.notable-trait-badge-container > .badge-2 { background: var(--notable-badge-orange); }
.notable-trait-badge-container > .badge-3 { background: var(--notable-badge-green); }
.notable-trait-badge-container > .badge-4 { background: var(--notable-badge-blue); }
.notable-trait-badge-container > .badge-5 { background: var(--notable-badge-violet); }

.rightside {
padding-left: 12px;
float: right;
Expand Down Expand Up @@ -3279,6 +3309,12 @@ by default.
--scrape-example-code-wrapper-background-end: rgba(255, 255, 255, 0);
--sidebar-resizer-hover: hsl(207, 90%, 66%);
--sidebar-resizer-active: hsl(207, 90%, 54%);
--notable-badge-pink: oklch(0.88 0.21 0);
--notable-badge-red: oklch(0.88 0.21 40);
--notable-badge-orange: oklch(0.88 0.21 70);
--notable-badge-green: oklch(0.88 0.21 150);
--notable-badge-blue: oklch(0.88 0.21 240);
--notable-badge-violet: oklch(0.88 0.21 300);
}
/* End theme: light */

Expand Down Expand Up @@ -3389,6 +3425,12 @@ by default.
--scrape-example-code-wrapper-background-end: rgba(53, 53, 53, 0);
--sidebar-resizer-hover: hsl(207, 30%, 54%);
--sidebar-resizer-active: hsl(207, 90%, 54%);
--notable-badge-pink: oklch(0.55 0.21 0);
--notable-badge-red: oklch(0.55 0.21 40);
--notable-badge-orange: oklch(0.55 0.21 70);
--notable-badge-green: oklch(0.55 0.21 150);
--notable-badge-blue: oklch(0.55 0.21 240);
--notable-badge-violet: oklch(0.55 0.21 300);
}
/* End theme: dark */

Expand Down Expand Up @@ -3503,6 +3545,12 @@ Original by Dempfi (https://github.com/dempfi/ayu)
--scrape-example-code-wrapper-background-end: rgba(15, 20, 25, 0);
--sidebar-resizer-hover: hsl(34, 50%, 33%);
--sidebar-resizer-active: hsl(34, 100%, 66%);
--notable-badge-pink: oklch(0.49 0.21 0);
--notable-badge-red: oklch(0.49 0.21 40);
--notable-badge-orange: oklch(0.49 0.21 70);
--notable-badge-green: oklch(0.49 0.21 150);
--notable-badge-blue: oklch(0.49 0.21 240);
--notable-badge-violet: oklch(0.49 0.21 300);
}

:root[data-theme="ayu"] h1,
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
4 changes: 4 additions & 0 deletions tests/rustdoc-html/notable-trait/auxiliary/notable-dep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#![feature(doc_notable_trait)]

#[doc(notable_trait)]
pub trait Spaceship {}
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> {}
15 changes: 15 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,15 @@
#![feature(doc_notable_trait, negative_impls)]
#![crate_name = "foo"]

#[doc(notable_trait)]
pub trait Neg {}
#[doc(notable_trait)]
pub trait Pos {}

// A negative impl must not produce a badge.
//@ has 'foo/struct.T.html'
//@ count - '//div[@class="notable-trait-badge-container"]/a' 1
//@ has - '//div[@class="notable-trait-badge-container"]/a[@href="trait.Pos.html"]' 'Pos'
pub struct T;
impl !Neg for T {}
impl Pos for T {}
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 {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//@ aux-build:notable-dep.rs

#![crate_name = "foo"]

extern crate notable_dep;

// A notable trait from a dependency that was compiled but not documented is
// unlinkable: the badge is still emitted but rendered as plain text.
use notable_dep::Spaceship;

//@ has 'foo/struct.Rocket.html'
// The badge is present...
//@ has - '//div[@class="notable-trait-badge-container"]/a' 'Spaceship'
// ...but unlinked: no badge carries an `href`.
//@ count - '//div[@class="notable-trait-badge-container"]/a[@href]' 0
pub struct Rocket;
impl Spaceship for Rocket {}
21 changes: 21 additions & 0 deletions tests/rustdoc-html/notable-trait/notable-trait-badge-unlinkable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#![feature(doc_notable_trait)]
#![crate_name = "foo"]

// Doc-hidden traits don't get badges.
#[doc(notable_trait)]
#[doc(hidden)]
pub trait Hidden {}

// Private traits don't get badges.
#[doc(notable_trait)]
trait Private {}
#[doc(notable_trait)]
pub trait Public {}

//@ has 'foo/struct.Foo.html'
//@ count - '//div[@class="notable-trait-badge-container"]' 1
//@ has - '//div[@class="notable-trait-badge-container"]/a[@href="trait.Public.html"]' 'Public'
pub struct Foo;
impl Hidden for Foo {}
impl Private for Foo {}
impl Public for Foo {}
Loading
Loading