about summary refs log tree commit diff
path: root/src/librustdoc/html
diff options
context:
space:
mode:
Diffstat (limited to 'src/librustdoc/html')
-rw-r--r--src/librustdoc/html/format.rs39
-rw-r--r--src/librustdoc/html/markdown.rs15
-rw-r--r--src/librustdoc/html/render/mod.rs7
-rw-r--r--src/librustdoc/html/render/print_item.rs29
-rw-r--r--src/librustdoc/html/render/search_index.rs23
-rw-r--r--src/librustdoc/html/static/css/rustdoc.css12
-rw-r--r--src/librustdoc/html/static/js/main.js148
7 files changed, 206 insertions, 67 deletions
diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index d963d6092c4..f26d74629dd 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -1257,9 +1257,9 @@ impl clean::Impl {
                 };
                 primitive_link_fragment(f, PrimitiveType::Tuple, &format!("fn ({name}₁, {name}₂, …, {name}ₙ{ellipsis})"), "#trait-implementations-1", cx)?;
                 // Write output.
-                if let clean::FnRetTy::Return(ty) = &bare_fn.decl.output {
+                if !bare_fn.decl.output.is_unit() {
                     write!(f, " -> ")?;
-                    fmt_type(ty, f, use_absolute, cx)?;
+                    fmt_type(&bare_fn.decl.output, f, use_absolute, cx)?;
                 }
             } else if let Some(ty) = self.kind.as_blanket_ty() {
                 fmt_type(ty, f, use_absolute, cx)?;
@@ -1296,22 +1296,6 @@ impl clean::Arguments {
     }
 }
 
-impl clean::FnRetTy {
-    pub(crate) fn print<'a, 'tcx: 'a>(
-        &'a self,
-        cx: &'a Context<'tcx>,
-    ) -> impl fmt::Display + 'a + Captures<'tcx> {
-        display_fn(move |f| match self {
-            clean::Return(clean::Tuple(tys)) if tys.is_empty() => Ok(()),
-            clean::Return(ty) if f.alternate() => {
-                write!(f, " -> {:#}", ty.print(cx))
-            }
-            clean::Return(ty) => write!(f, " -&gt; {}", ty.print(cx)),
-            clean::DefaultReturn => Ok(()),
-        })
-    }
-}
-
 impl clean::BareFunctionDecl {
     fn print_hrtb_with_space<'a, 'tcx: 'a>(
         &'a self,
@@ -1366,7 +1350,7 @@ impl clean::FnDecl {
                     "({args:#}{ellipsis}){arrow:#}",
                     args = self.inputs.print(cx),
                     ellipsis = ellipsis,
-                    arrow = self.output.print(cx)
+                    arrow = self.print_output(cx)
                 )
             } else {
                 write!(
@@ -1374,7 +1358,7 @@ impl clean::FnDecl {
                     "({args}{ellipsis}){arrow}",
                     args = self.inputs.print(cx),
                     ellipsis = ellipsis,
-                    arrow = self.output.print(cx)
+                    arrow = self.print_output(cx)
                 )
             }
         })
@@ -1464,9 +1448,22 @@ impl clean::FnDecl {
             Some(n) => write!(f, "\n{})", Indent(n))?,
         };
 
-        fmt::Display::fmt(&self.output.print(cx), f)?;
+        fmt::Display::fmt(&self.print_output(cx), f)?;
         Ok(())
     }
+
+    pub(crate) fn print_output<'a, 'tcx: 'a>(
+        &'a self,
+        cx: &'a Context<'tcx>,
+    ) -> impl fmt::Display + 'a + Captures<'tcx> {
+        display_fn(move |f| match &self.output {
+            clean::Tuple(tys) if tys.is_empty() => Ok(()),
+            ty if f.alternate() => {
+                write!(f, " -> {:#}", ty.print(cx))
+            }
+            ty => write!(f, " -&gt; {}", ty.print(cx)),
+        })
+    }
 }
 
 pub(crate) fn visibility_print_with_space<'a, 'tcx: 'a>(
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index b26a5c32ec6..1c27320024a 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -381,7 +381,6 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
             Some(Event::Code(text)) => {
                 trace!("saw code {}", text);
                 if let Some(link) = self.shortcut_link {
-                    trace!("original text was {}", link.original_text);
                     // NOTE: this only replaces if the code block is the *entire* text.
                     // If only part of the link has code highlighting, the disambiguator will not be removed.
                     // e.g. [fn@`f`]
@@ -390,8 +389,11 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
                     // So we could never be sure we weren't replacing too much:
                     // [fn@my_`f`unc] is treated the same as [my_func()] in that pass.
                     //
-                    // NOTE: &[1..len() - 1] is to strip the backticks
-                    if **text == link.original_text[1..link.original_text.len() - 1] {
+                    // NOTE: .get(1..len() - 1) is to strip the backticks
+                    if let Some(link) = self.links.iter().find(|l| {
+                        l.href == link.href
+                            && Some(&**text) == l.original_text.get(1..l.original_text.len() - 1)
+                    }) {
                         debug!("replacing {} with {}", text, link.new_text);
                         *text = CowStr::Borrowed(&link.new_text);
                     }
@@ -402,9 +404,12 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
             Some(Event::Text(text)) => {
                 trace!("saw text {}", text);
                 if let Some(link) = self.shortcut_link {
-                    trace!("original text was {}", link.original_text);
                     // NOTE: same limitations as `Event::Code`
-                    if **text == *link.original_text {
+                    if let Some(link) = self
+                        .links
+                        .iter()
+                        .find(|l| l.href == link.href && **text == *l.original_text)
+                    {
                         debug!("replacing {} with {}", text, link.new_text);
                         *text = CowStr::Borrowed(&link.new_text);
                     }
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 42e27d35a94..a5223bd6309 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -844,7 +844,7 @@ fn assoc_method(
         + name.as_str().len()
         + generics_len;
 
-    let notable_traits = d.output.as_return().and_then(|output| notable_traits_button(output, cx));
+    let notable_traits = notable_traits_button(&d.output, cx);
 
     let (indent, indent_str, end_newline) = if parent == ItemType::Trait {
         header_len += 4;
@@ -1282,6 +1282,11 @@ fn should_render_item(item: &clean::Item, deref_mut_: bool, tcx: TyCtxt<'_>) ->
 pub(crate) fn notable_traits_button(ty: &clean::Type, cx: &mut Context<'_>) -> Option<String> {
     let mut has_notable_trait = false;
 
+    if ty.is_unit() {
+        // Very common fast path.
+        return None;
+    }
+
     let did = ty.def_id(cx.cache())?;
 
     // Box has pass-through impls for Read, Write, Iterator, and Future when the
diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs
index d2dc47af7ac..21f61acb2c5 100644
--- a/src/librustdoc/html/render/print_item.rs
+++ b/src/librustdoc/html/render/print_item.rs
@@ -587,8 +587,7 @@ fn item_function(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, f: &cle
         + name.as_str().len()
         + generics_len;
 
-    let notable_traits =
-        f.decl.output.as_return().and_then(|output| notable_traits_button(output, cx));
+    let notable_traits = notable_traits_button(&f.decl.output, cx);
 
     wrap_item(w, |w| {
         w.reserve(header_len);
@@ -1420,30 +1419,36 @@ fn item_macro(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean:
     write!(w, "{}", document(cx, it, None, HeadingOffset::H2))
 }
 
-fn item_proc_macro(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, m: &clean::ProcMacro) {
-    wrap_item(w, |w| {
+fn item_proc_macro(
+    w: &mut impl fmt::Write,
+    cx: &mut Context<'_>,
+    it: &clean::Item,
+    m: &clean::ProcMacro,
+) {
+    let mut buffer = Buffer::new();
+    wrap_item(&mut buffer, |buffer| {
         let name = it.name.expect("proc-macros always have names");
         match m.kind {
             MacroKind::Bang => {
-                write!(w, "{}!() {{ /* proc-macro */ }}", name);
+                write!(buffer, "{}!() {{ /* proc-macro */ }}", name);
             }
             MacroKind::Attr => {
-                write!(w, "#[{}]", name);
+                write!(buffer, "#[{}]", name);
             }
             MacroKind::Derive => {
-                write!(w, "#[derive({})]", name);
+                write!(buffer, "#[derive({})]", name);
                 if !m.helpers.is_empty() {
-                    w.push_str("\n{\n");
-                    w.push_str("    // Attributes available to this derive:\n");
+                    buffer.push_str("\n{\n");
+                    buffer.push_str("    // Attributes available to this derive:\n");
                     for attr in &m.helpers {
-                        writeln!(w, "    #[{}]", attr);
+                        writeln!(buffer, "    #[{}]", attr);
                     }
-                    w.push_str("}\n");
+                    buffer.push_str("}\n");
                 }
             }
         }
     });
-    write!(w, "{}", document(cx, it, None, HeadingOffset::H2))
+    write!(w, "{}{}", buffer.into_inner(), document(cx, it, None, HeadingOffset::H2)).unwrap();
 }
 
 fn item_primitive(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item) {
diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs
index 846299f02e3..f34be120d29 100644
--- a/src/librustdoc/html/render/search_index.rs
+++ b/src/librustdoc/html/render/search_index.rs
@@ -7,7 +7,7 @@ use rustc_span::symbol::Symbol;
 use serde::ser::{Serialize, SerializeStruct, Serializer};
 
 use crate::clean;
-use crate::clean::types::{FnRetTy, Function, Generics, ItemId, Type, WherePredicate};
+use crate::clean::types::{Function, Generics, ItemId, Type, WherePredicate};
 use crate::formats::cache::{Cache, OrphanImplItem};
 use crate::formats::item_type::ItemType;
 use crate::html::format::join_with_double_colon;
@@ -656,22 +656,9 @@ fn get_fn_inputs_and_outputs<'tcx>(
     }
 
     let mut ret_types = Vec::new();
-    match decl.output {
-        FnRetTy::Return(ref return_type) => {
-            add_generics_and_bounds_as_types(
-                self_,
-                generics,
-                return_type,
-                tcx,
-                0,
-                &mut ret_types,
-                cache,
-            );
-            if ret_types.is_empty() {
-                ret_types.push(get_index_type(return_type, vec![]));
-            }
-        }
-        _ => {}
-    };
+    add_generics_and_bounds_as_types(self_, generics, &decl.output, tcx, 0, &mut ret_types, cache);
+    if ret_types.is_empty() {
+        ret_types.push(get_index_type(&decl.output, vec![]));
+    }
     (all_types, ret_types)
 }
diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index a7d5f497756..054cfe7597e 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -1179,6 +1179,10 @@ a.test-arrow:hover {
 	position: relative;
 }
 
+.code-header a.tooltip:hover {
+	color: var(--link-color);
+}
+
 /* placeholder thunk so that the mouse can easily travel from "(i)" to popover
 	the resulting "hover tunnel" is a stepped triangle, approximating
 	https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown */
@@ -1191,6 +1195,14 @@ a.tooltip:hover::after {
 	content: "\00a0";
 }
 
+/* This animation is layered onto the mistake-proofing delay for dismissing
+	a hovered tooltip, to ensure it feels responsive even with the delay.
+	*/
+.fade-out {
+	opacity: 0;
+	transition: opacity 0.45s cubic-bezier(0, 0, 0.1, 1.0);
+}
+
 .popover.tooltip .content {
 	margin: 0.25em 0.5em;
 }
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index bccf675c14b..6da51ea0a55 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -4,6 +4,13 @@
 
 "use strict";
 
+// The amount of time that the cursor must remain still over a hover target before
+// revealing a tooltip.
+//
+// https://www.nngroup.com/articles/timing-exposing-content/
+window.RUSTDOC_TOOLTIP_HOVER_MS = 300;
+window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS = 450;
+
 // Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL
 // for a resource under the root-path, with the resource-suffix.
 function resourcePath(basename, extension) {
@@ -772,6 +779,13 @@ function preLoadCss(cssUrl) {
         });
     });
 
+    /**
+     * Show a tooltip immediately.
+     *
+     * @param {DOMElement} e - The tooltip's anchor point. The DOM is consulted to figure
+     *                         out what the tooltip should contain, and where it should be
+     *                         positioned.
+     */
     function showTooltip(e) {
         const notable_ty = e.getAttribute("data-notable-ty");
         if (!window.NOTABLE_TRAITS && notable_ty) {
@@ -782,8 +796,10 @@ function preLoadCss(cssUrl) {
                 throw new Error("showTooltip() called with notable without any notable traits!");
             }
         }
+        // Make this function idempotent. If the tooltip is already shown, avoid doing extra work
+        // and leave it alone.
         if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) {
-            // Make this function idempotent.
+            clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
             return;
         }
         window.hideAllModals(false);
@@ -791,11 +807,18 @@ function preLoadCss(cssUrl) {
         if (notable_ty) {
             wrapper.innerHTML = "<div class=\"content\">" +
                 window.NOTABLE_TRAITS[notable_ty] + "</div>";
-        } else if (e.getAttribute("title") !== undefined) {
-            const titleContent = document.createElement("div");
-            titleContent.className = "content";
-            titleContent.appendChild(document.createTextNode(e.getAttribute("title")));
-            wrapper.appendChild(titleContent);
+        } else {
+            // Replace any `title` attribute with `data-title` to avoid double tooltips.
+            if (e.getAttribute("title") !== null) {
+                e.setAttribute("data-title", e.getAttribute("title"));
+                e.removeAttribute("title");
+            }
+            if (e.getAttribute("data-title") !== null) {
+                const titleContent = document.createElement("div");
+                titleContent.className = "content";
+                titleContent.appendChild(document.createTextNode(e.getAttribute("data-title")));
+                wrapper.appendChild(titleContent);
+            }
         }
         wrapper.className = "tooltip popover";
         const focusCatcher = document.createElement("div");
@@ -824,17 +847,77 @@ function preLoadCss(cssUrl) {
         wrapper.style.visibility = "";
         window.CURRENT_TOOLTIP_ELEMENT = wrapper;
         window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE = e;
+        clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
+        wrapper.onpointerenter = function(ev) {
+            // If this is a synthetic touch event, ignore it. A click event will be along shortly.
+            if (ev.pointerType !== "mouse") {
+                return;
+            }
+            clearTooltipHoverTimeout(e);
+        };
         wrapper.onpointerleave = function(ev) {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
             if (ev.pointerType !== "mouse") {
                 return;
             }
-            if (!e.TOOLTIP_FORCE_VISIBLE && !elemIsInParent(event.relatedTarget, e)) {
-                hideTooltip(true);
+            if (!e.TOOLTIP_FORCE_VISIBLE && !elemIsInParent(ev.relatedTarget, e)) {
+                // See "Tooltip pointer leave gesture" below.
+                setTooltipHoverTimeout(e, false);
+                addClass(wrapper, "fade-out");
             }
         };
     }
 
+    /**
+     * Show or hide the tooltip after a timeout. If a timeout was already set before this function
+     * was called, that timeout gets cleared. If the tooltip is already in the requested state,
+     * this function will still clear any pending timeout, but otherwise do nothing.
+     *
+     * @param {DOMElement} element - The tooltip's anchor point. The DOM is consulted to figure
+     *                               out what the tooltip should contain, and where it should be
+     *                               positioned.
+     * @param {boolean}    show    - If true, the tooltip will be made visible. If false, it will
+     *                               be hidden.
+     */
+    function setTooltipHoverTimeout(element, show) {
+        clearTooltipHoverTimeout(element);
+        if (!show && !window.CURRENT_TOOLTIP_ELEMENT) {
+            // To "hide" an already hidden element, just cancel its timeout.
+            return;
+        }
+        if (show && window.CURRENT_TOOLTIP_ELEMENT) {
+            // To "show" an already visible element, just cancel its timeout.
+            return;
+        }
+        if (window.CURRENT_TOOLTIP_ELEMENT &&
+            window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE !== element) {
+            // Don't do anything if another tooltip is already visible.
+            return;
+        }
+        element.TOOLTIP_HOVER_TIMEOUT = setTimeout(() => {
+            if (show) {
+                showTooltip(element);
+            } else if (!element.TOOLTIP_FORCE_VISIBLE) {
+                hideTooltip(false);
+            }
+        }, show ? window.RUSTDOC_TOOLTIP_HOVER_MS : window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS);
+    }
+
+    /**
+     * If a show/hide timeout was set by `setTooltipHoverTimeout`, cancel it. If none exists,
+     * do nothing.
+     *
+     * @param {DOMElement} element - The tooltip's anchor point,
+     *                               as passed to `setTooltipHoverTimeout`.
+     */
+    function clearTooltipHoverTimeout(element) {
+        if (element.TOOLTIP_HOVER_TIMEOUT !== undefined) {
+            removeClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out");
+            clearTimeout(element.TOOLTIP_HOVER_TIMEOUT);
+            delete element.TOOLTIP_HOVER_TIMEOUT;
+        }
+    }
+
     function tooltipBlurHandler(event) {
         if (window.CURRENT_TOOLTIP_ELEMENT &&
             !elemIsInParent(document.activeElement, window.CURRENT_TOOLTIP_ELEMENT) &&
@@ -854,6 +937,12 @@ function preLoadCss(cssUrl) {
         }
     }
 
+    /**
+     * Hide the current tooltip immediately.
+     *
+     * @param {boolean} focus - If set to `true`, move keyboard focus to the tooltip anchor point.
+     *                          If set to `false`, leave keyboard focus alone.
+     */
     function hideTooltip(focus) {
         if (window.CURRENT_TOOLTIP_ELEMENT) {
             if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) {
@@ -864,6 +953,7 @@ function preLoadCss(cssUrl) {
             }
             const body = document.getElementsByTagName("body")[0];
             body.removeChild(window.CURRENT_TOOLTIP_ELEMENT);
+            clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
             window.CURRENT_TOOLTIP_ELEMENT = null;
         }
     }
@@ -886,7 +976,14 @@ function preLoadCss(cssUrl) {
             if (ev.pointerType !== "mouse") {
                 return;
             }
-            showTooltip(this);
+            setTooltipHoverTimeout(this, true);
+        };
+        e.onpointermove = function(ev) {
+            // If this is a synthetic touch event, ignore it. A click event will be along shortly.
+            if (ev.pointerType !== "mouse") {
+                return;
+            }
+            setTooltipHoverTimeout(this, true);
         };
         e.onpointerleave = function(ev) {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
@@ -895,7 +992,38 @@ function preLoadCss(cssUrl) {
             }
             if (!this.TOOLTIP_FORCE_VISIBLE &&
                 !elemIsInParent(ev.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT)) {
-                hideTooltip(true);
+                // Tooltip pointer leave gesture:
+                //
+                // Designing a good hover microinteraction is a matter of guessing user
+                // intent from what are, literally, vague gestures. In this case, guessing if
+                // hovering in or out of the tooltip base is intentional or not.
+                //
+                // To figure this out, a few different techniques are used:
+                //
+                // * When the mouse pointer enters a tooltip anchor point, its hitbox is grown
+                //   on the bottom, where the popover is/will appear. Search "hover tunnel" in
+                //   rustdoc.css for the implementation.
+                // * There's a delay when the mouse pointer enters the popover base anchor, in
+                //   case the mouse pointer was just passing through and the user didn't want
+                //   to open it.
+                // * Similarly, a delay is added when exiting the anchor, or the popover
+                //   itself, before hiding it.
+                // * A fade-out animation is layered onto the pointer exit delay to immediately
+                //   inform the user that they successfully dismissed the popover, while still
+                //   providing a way for them to cancel it if it was a mistake and they still
+                //   wanted to interact with it.
+                // * No animation is used for revealing it, because we don't want people to try
+                //   to interact with an element while it's in the middle of fading in: either
+                //   they're allowed to interact with it while it's fading in, meaning it can't
+                //   serve as mistake-proofing for the popover, or they can't, but
+                //   they might try and be frustrated.
+                //
+                // See also:
+                // * https://www.nngroup.com/articles/timing-exposing-content/
+                // * https://www.nngroup.com/articles/tooltip-guidelines/
+                // * https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown
+                setTooltipHoverTimeout(e, false);
+                addClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out");
             }
         };
     });