Skip to content
Open
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
44 changes: 43 additions & 1 deletion 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 @@ -1790,6 +1790,48 @@ 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 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::<BTreeMap<String, NotableTraitBadge>>()
.into_values()
.collect()
}

#[derive(Clone, Copy, Debug)]
struct ImplRenderingParameters {
show_def_docs: bool,
Expand Down
36 changes: 35 additions & 1 deletion src/librustdoc/html/render/print_item.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 = "<dl class=\"item-table\">";
Expand All @@ -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,
Comment thread
ThierryBerger marked this conversation as resolved.
/// Pre-rendered `style="..."` attribute.
style_attr: String,
}

#[derive(Template)]
#[template(path = "print_item.html")]
struct ItemVars<'a> {
Expand All @@ -58,6 +69,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 @@ -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<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);
// 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 {
Expand All @@ -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(),
};

Expand Down
19 changes: 19 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,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;
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" {# #} {% if !badge.href.is_empty()
%}href="{{badge.href|safe}}" {% endif %} {#+ #} title="{{badge.full_path}}" {#+ #}
{{badge.style_attr|safe}}>{{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' '//a[@class="notable-trait-badge"]' '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 {}
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 - '//a[@class="notable-trait-badge"]' 1
//@ has - '//a[@class="notable-trait-badge"]' '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 - '//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 {}
Loading