diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index 41c506f33dc..e936e1ca07e 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -16,6 +16,24 @@
--src-sidebar-width: 300px;
--desktop-sidebar-z-index: 100;
--sidebar-elems-left-padding: 24px;
+ /* clipboard */
+ --clipboard-image: url('data:image/svg+xml,');
+ --copy-path-height: 34px;
+ --copy-path-width: 33px;
+ /* Checkmark */
+ --checkmark-image: url('data:image/svg+xml,');
+ --button-left-margin: 4px;
+ --button-border-radius: 2px;
}
/* See FiraSans-LICENSE.txt for the Fira Sans license. */
@@ -723,6 +741,11 @@ ul.block, .block li {
position: relative;
margin-bottom: 10px;
}
+
+.rustdoc .example-wrap > pre {
+ border-radius: 6px;
+}
+
/* For the last child of a div, the margin will be taken care of
by the margin-top of the next item. */
.rustdoc .example-wrap:last-child {
@@ -1427,15 +1450,17 @@ documentation. */
top: 20px;
}
-a.test-arrow {
+.example-wrap > a.test-arrow, .example-wrap .button-holder {
visibility: hidden;
position: absolute;
- padding: 5px 10px 5px 10px;
- border-radius: 5px;
- font-size: 1.375rem;
- top: 5px;
- right: 5px;
+ top: 4px;
+ right: 4px;
z-index: 1;
+}
+a.test-arrow {
+ padding: 5px 7px;
+ border-radius: var(--button-border-radius);
+ font-size: 1rem;
color: var(--test-arrow-color);
background-color: var(--test-arrow-background-color);
}
@@ -1443,9 +1468,37 @@ a.test-arrow:hover {
color: var(--test-arrow-hover-color);
background-color: var(--test-arrow-hover-background-color);
}
-.example-wrap:hover .test-arrow {
+.example-wrap .button-holder {
+ display: flex;
+}
+.example-wrap:hover > .test-arrow {
+ padding: 2px 7px;
+}
+.example-wrap:hover > .test-arrow, .example-wrap:hover > .button-holder {
visibility: visible;
}
+.example-wrap .button-holder .copy-button {
+ color: var(--copy-path-button-color);
+ background: var(--main-background-color);
+ height: var(--copy-path-height);
+ width: var(--copy-path-width);
+ margin-left: var(--button-left-margin);
+ padding: 2px 0 0 4px;
+ border: 0;
+ cursor: pointer;
+ border-radius: var(--button-border-radius);
+}
+.example-wrap .button-holder .copy-button::before {
+ filter: var(--copy-path-img-filter);
+ content: var(--clipboard-image);
+}
+.example-wrap .button-holder .copy-button:hover::before {
+ filter: var(--copy-path-img-hover-filter);
+}
+.example-wrap .button-holder .copy-button.clicked::before {
+ content: var(--checkmark-image);
+ padding-right: 5px;
+}
.code-attribute {
font-weight: 300;
@@ -1610,7 +1663,7 @@ a.tooltip:hover::after {
}
#settings-menu, #help-button {
- margin-left: 4px;
+ margin-left: var(--button-left-margin);
display: flex;
}
#sidebar-button {
@@ -1641,7 +1694,7 @@ a.tooltip:hover::after {
justify-content: center;
background-color: var(--button-background-color);
border: 1px solid var(--border-color);
- border-radius: 2px;
+ border-radius: var(--button-border-radius);
color: var(--settings-button-color);
/* Rare exception to specifying font sizes in rem. Since this is acting
as an icon, it's okay to specify their sizes in pixels. */
@@ -1693,8 +1746,8 @@ a.tooltip:hover::after {
#copy-path {
color: var(--copy-path-button-color);
background: var(--main-background-color);
- height: 34px;
- width: 33px;
+ height: var(--copy-path-height);
+ width: var(--copy-path-width);
margin-left: 10px;
padding: 0;
padding-left: 2px;
@@ -1703,27 +1756,13 @@ a.tooltip:hover::after {
}
#copy-path::before {
filter: var(--copy-path-img-filter);
- /* clipboard */
- content: url('data:image/svg+xml,');
- width: 19px;
- height: 18px;
+ content: var(--clipboard-image);
}
#copy-path:hover::before {
filter: var(--copy-path-img-hover-filter);
}
#copy-path.clicked::before {
- /* Checkmark */
- content: url('data:image/svg+xml,');
+ content: var(--checkmark-image);
}
@keyframes rotating {
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index 9506bc9ed22..40d65ae7910 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -1771,9 +1771,37 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
}());
// This section handles the copy button that appears next to the path breadcrumbs
+// and the copy buttons on the code examples.
(function() {
- let reset_button_timeout = null;
+ // Common functions to copy buttons.
+ function copyContentToClipboard(content) {
+ const el = document.createElement("textarea");
+ el.value = content;
+ el.setAttribute("readonly", "");
+ // To not make it appear on the screen.
+ el.style.position = "absolute";
+ el.style.left = "-9999px";
+ document.body.appendChild(el);
+ el.select();
+ document.execCommand("copy");
+ document.body.removeChild(el);
+ }
+
+ function copyButtonAnimation(button) {
+ button.classList.add("clicked");
+
+ if (button.reset_button_timeout !== undefined) {
+ window.clearTimeout(button.reset_button_timeout);
+ }
+
+ button.reset_button_timeout = window.setTimeout(() => {
+ button.reset_button_timeout = undefined;
+ button.classList.remove("clicked");
+ }, 1000);
+ }
+
+ // Copy button that appears next to the path breadcrumbs.
const but = document.getElementById("copy-path");
if (!but) {
return;
@@ -1788,29 +1816,49 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
}
});
- const el = document.createElement("textarea");
- el.value = path.join("::");
- el.setAttribute("readonly", "");
- // To not make it appear on the screen.
- el.style.position = "absolute";
- el.style.left = "-9999px";
-
- document.body.appendChild(el);
- el.select();
- document.execCommand("copy");
- document.body.removeChild(el);
-
- but.classList.add("clicked");
-
- if (reset_button_timeout !== null) {
- window.clearTimeout(reset_button_timeout);
- }
-
- function reset_button() {
- reset_button_timeout = null;
- but.classList.remove("clicked");
- }
-
- reset_button_timeout = window.setTimeout(reset_button, 1000);
+ copyContentToClipboard(path.join("::"));
+ copyButtonAnimation(but);
};
+
+ // Copy buttons on code examples.
+ function copyCode(codeElem) {
+ if (!codeElem) {
+ // Should never happen, but the world is a dark and dangerous place.
+ return;
+ }
+ copyContentToClipboard(codeElem.textContent);
+ }
+
+ function addCopyButton(event) {
+ let elem = event.target;
+ while (!hasClass(elem, "example-wrap")) {
+ elem = elem.parentElement;
+ if (elem.tagName === "body" || hasClass(elem, "docblock")) {
+ return;
+ }
+ }
+ // Since the button will be added, no need to keep this listener around.
+ elem.removeEventListener("mouseover", addCopyButton);
+
+ const parent = document.createElement("div");
+ parent.className = "button-holder";
+ const runButton = elem.querySelector(".test-arrow");
+ if (runButton !== null) {
+ // If there is a run button, we move it into the same div.
+ parent.appendChild(runButton);
+ }
+ elem.appendChild(parent);
+ const copyButton = document.createElement("button");
+ copyButton.className = "copy-button";
+ copyButton.title = "Copy code to clipboard";
+ copyButton.addEventListener("click", () => {
+ copyCode(elem.querySelector("pre > code"));
+ copyButtonAnimation(copyButton);
+ });
+ parent.appendChild(copyButton);
+ }
+
+ onEachLazy(document.querySelectorAll(".docblock .example-wrap"), elem => {
+ elem.addEventListener("mouseover", addCopyButton);
+ });
}());
diff --git a/tests/rustdoc-gui/copy-code.goml b/tests/rustdoc-gui/copy-code.goml
new file mode 100644
index 00000000000..72a5bece175
--- /dev/null
+++ b/tests/rustdoc-gui/copy-code.goml
@@ -0,0 +1,54 @@
+// Checks that the "copy code" button is not triggering JS error and its display
+// isn't broken.
+go-to: "file://" + |DOC_PATH| + "/test_docs/fn.foo.html"
+
+define-function: (
+ "check-copy-button",
+ [],
+ block {
+ // First we ensure that there are no "copy code" currently existing.
+ assert-count: (".example-wrap .copy-button", 0)
+ move-cursor-to: ".example-wrap"
+ assert-count: (".example-wrap .copy-button", 1)
+ // We now ensure it's only displayed when the example is hovered.
+ assert-css: (".example-wrap .copy-button", { "visibility": "visible" })
+ move-cursor-to: ".search-input"
+ assert-css: (".example-wrap .copy-button", { "visibility": "hidden" })
+ // Checking that the copy button has the same size as the "copy path" button.
+ compare-elements-size: (
+ "#copy-path",
+ ".example-wrap:nth-of-type(1) .copy-button",
+ ["height", "width"],
+ )
+ },
+)
+
+call-function: ("check-copy-button", {})
+// Checking that the run button and the copy button have the same height.
+compare-elements-size: (
+ ".example-wrap:nth-of-type(1) .test-arrow",
+ ".example-wrap:nth-of-type(1) .copy-button",
+ ["height"],
+)
+// ... and the same y position.
+compare-elements-position: (
+ ".example-wrap:nth-of-type(1) .test-arrow",
+ ".example-wrap:nth-of-type(1) .copy-button",
+ ["y"],
+)
+store-size: (".example-wrap:nth-of-type(1) .copy-button", {
+ "height": copy_height,
+ "width": copy_width,
+})
+assert: |copy_height| > 0 && |copy_width| > 0
+
+// Checking same things for the copy button when there is no run button.
+go-to: "file://" + |DOC_PATH| + "/lib2/sub_mod/struct.Foo.html"
+call-function: ("check-copy-button", {})
+// Ensure there is no run button.
+assert-count: (".example-wrap .test-arrow", 0)
+// Check it's the same size without a run button.
+assert-size: (".example-wrap:nth-of-type(1) .copy-button", {
+ "height": |copy_height|,
+ "width": |copy_width|,
+})
diff --git a/tests/rustdoc-gui/run-on-hover.goml b/tests/rustdoc-gui/run-on-hover.goml
index 087dd3374f8..b62da79b780 100644
--- a/tests/rustdoc-gui/run-on-hover.goml
+++ b/tests/rustdoc-gui/run-on-hover.goml
@@ -17,16 +17,16 @@ define-function: (
"visibility": "visible",
"color": |color|,
"background-color": |background|,
- "font-size": "22px",
- "border-radius": "5px",
+ "font-size": "16px",
+ "border-radius": "2px",
})
move-cursor-to: ".test-arrow"
assert-css: (".test-arrow:hover", {
"visibility": "visible",
"color": |hover_color|,
"background-color": |hover_background|,
- "font-size": "22px",
- "border-radius": "5px",
+ "font-size": "16px",
+ "border-radius": "2px",
})
},
)