diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index f16f375a5a84b..03ece977529d3 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 5558c36f1d43d..a930a9c8e55ce 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; @@ -1790,6 +1790,48 @@ fn notable_traits_json<'a>(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 Some(did) = item.def_id() else { return Vec::new() }; + + if Some(did) == cx.tcx().lang_items().owned_box() + || Some(did) == cx.tcx().lang_items().pin_type() + { + return Vec::new(); + } + + let Some(impls) = cx.cache().impls.get(&did) else { return Vec::new() }; + + 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(cx.tcx()) { + return None; + } + let name = cx.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(_) => (cx.tcx().def_path_str(trait_did), None), + }; + Some((name.clone(), NotableTraitBadge { name, full_path, href })) + }) + .collect::>() + .into_values() + .collect() +} + #[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 7ade72429cb4d..762d1a738d2dc 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -1,5 +1,7 @@ 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; @@ -37,7 +39,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 = "
"; @@ -50,6 +52,15 @@ struct PathComponent { name: Symbol, } +struct NotableTraitBadgeVars { + name: String, + full_path: String, + /// Relative URL to the trait page, or empty when not linkable. + href: String, + /// Pre-rendered `style="..."` attribute. + style_attr: String, +} + #[derive(Template)] #[template(path = "print_item.html")] struct ItemVars<'a> { @@ -58,6 +69,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>, } @@ -111,6 +123,27 @@ 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); + // Evenly-spaced OKLCH hues at fixed light/chroma. + const BADGE_HUES: u64 = 8; + let hue = (h.finish() % BADGE_HUES) * 360 / BADGE_HUES; + let style_attr = format!("style=\"background: oklch(0.55 0.21 {hue})\""); + NotableTraitBadgeVars { + name: info.name, + full_path: info.full_path, + href: info.href.unwrap_or_default(), + style_attr, + } + }) + .collect(); + let path_components = if item.is_fake_item() { vec![] } else { @@ -134,6 +167,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..4966bcf99fb26 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -1648,6 +1648,25 @@ 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; +} + .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..80960d25b09bf 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/notable-trait-badge-generic.rs b/tests/rustdoc-html/notable-trait/notable-trait-badge-generic.rs new file mode 100644 index 0000000000000..d1618f155b1e8 --- /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' '//a[@class="notable-trait-badge"]' '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..d2e8a72bf5f5f --- /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 - '//a[@class="notable-trait-badge"]' 1 +//@ has - '//a[@class="notable-trait-badge"]' 'Base' +pub struct S; +impl Base for S {} +impl Derived for S {} 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..14fa51273e28b --- /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 - '//a[@class="notable-trait-badge"][@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 {}