diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index cc6cf2f8a648c..55b6343e7f015 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -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]. diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 9a08e76604df2..16a32188e35da 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -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; @@ -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 { if ty.is_unit() { // Very common fast path. @@ -1672,13 +1681,7 @@ fn notable_traits_button(ty: &clean::Type, cx: &Context<'_>) -> Option(tys: impl Iterator, 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, +} + +/// 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 { + 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::>() + .into_values() + .collect() + } else { + Vec::new() + } +} + #[derive(Clone, Copy, Debug)] struct ImplRenderingParameters { show_def_docs: bool, diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 7c1cc4ba7f9d9..7aeed50fdff51 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -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; @@ -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 = "
"; @@ -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, + /// Index of the `.notable-trait-badge-{n}` color class. + color_index: u8, +} + #[derive(Template)] #[template(path = "print_item.html")] struct ItemVars<'a> { @@ -59,6 +70,7 @@ struct ItemVars<'a> { item_type: &'a str, path_components: Vec, stability_since_raw: &'a str, + notable_trait_badges: Vec, src_href: Option<&'a str>, } @@ -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 = 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 { @@ -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(), }; diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 1090d4e2feb01..1e065cee87595 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -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; diff --git a/src/librustdoc/html/templates/print_item.html b/src/librustdoc/html/templates/print_item.html index 640fd3dfee498..3b36b36d283e5 100644 --- a/src/librustdoc/html/templates/print_item.html +++ b/src/librustdoc/html/templates/print_item.html @@ -20,6 +20,15 @@

{# #} {# #} + {% if !notable_trait_badges.is_empty() %} +
+ {% for badge in notable_trait_badges.iter() %} + {{badge.name}} + {% endfor %} +
+ {% endif %} {% if !stability_since_raw.is_empty() %} {{ stability_since_raw|safe +}} {% endif %} diff --git a/tests/rustdoc-html/notable-trait/auxiliary/notable-dep.rs b/tests/rustdoc-html/notable-trait/auxiliary/notable-dep.rs new file mode 100644 index 0000000000000..391e6771443c9 --- /dev/null +++ b/tests/rustdoc-html/notable-trait/auxiliary/notable-dep.rs @@ -0,0 +1,4 @@ +#![feature(doc_notable_trait)] + +#[doc(notable_trait)] +pub trait Spaceship {} diff --git a/tests/rustdoc-html/notable-trait/notable-trait-badge-generic.rs b/tests/rustdoc-html/notable-trait/notable-trait-badge-generic.rs new file mode 100644 index 0000000000000..723f3fed6c5a5 --- /dev/null +++ b/tests/rustdoc-html/notable-trait/notable-trait-badge-generic.rs @@ -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(pub T); +impl Labeled for Wrapper {} diff --git a/tests/rustdoc-html/notable-trait/notable-trait-badge-negative.rs b/tests/rustdoc-html/notable-trait/notable-trait-badge-negative.rs new file mode 100644 index 0000000000000..525393da8ab30 --- /dev/null +++ b/tests/rustdoc-html/notable-trait/notable-trait-badge-negative.rs @@ -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 {} diff --git a/tests/rustdoc-html/notable-trait/notable-trait-badge-supertrait.rs b/tests/rustdoc-html/notable-trait/notable-trait-badge-supertrait.rs new file mode 100644 index 0000000000000..9bb3226099a51 --- /dev/null +++ b/tests/rustdoc-html/notable-trait/notable-trait-badge-supertrait.rs @@ -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 {} diff --git a/tests/rustdoc-html/notable-trait/notable-trait-badge-unlinkable-cross-crate.rs b/tests/rustdoc-html/notable-trait/notable-trait-badge-unlinkable-cross-crate.rs new file mode 100644 index 0000000000000..de7008cf5ab0c --- /dev/null +++ b/tests/rustdoc-html/notable-trait/notable-trait-badge-unlinkable-cross-crate.rs @@ -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 {} diff --git a/tests/rustdoc-html/notable-trait/notable-trait-badge-unlinkable.rs b/tests/rustdoc-html/notable-trait/notable-trait-badge-unlinkable.rs new file mode 100644 index 0000000000000..f001ed756379b --- /dev/null +++ b/tests/rustdoc-html/notable-trait/notable-trait-badge-unlinkable.rs @@ -0,0 +1,16 @@ +#![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 {} + +//@ count 'foo/struct.Foo.html' '//div[@class="notable-trait-badge-container"]' 0 +pub struct Foo; +impl Hidden for Foo {} +impl Private for Foo {} diff --git a/tests/rustdoc-html/notable-trait/notable-trait-badge.rs b/tests/rustdoc-html/notable-trait/notable-trait-badge.rs new file mode 100644 index 0000000000000..b53d989d0f819 --- /dev/null +++ b/tests/rustdoc-html/notable-trait/notable-trait-badge.rs @@ -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 {}