Skip to content

Commit

Permalink
Merge pull request #107 from google/speaker-notes
Browse files Browse the repository at this point in the history
Add support for speaker notes
  • Loading branch information
mgeisler authored Jan 5, 2023
2 parents 28942d7 + d5359fa commit 15f88b3
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 2 deletions.
4 changes: 2 additions & 2 deletions book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class = "bob"

[output.html]
curly-quotes = true
additional-js = ["ga4.js"]
additional-css = ["svgbob.css"]
additional-js = ["ga4.js", "speaker-notes.js"]
additional-css = ["svgbob.css", "speaker-notes.css"]
git-repository-url = "https://github.com/google/comprehensive-rust"
edit-url-template = "https://github.com/google/comprehensive-rust/edit/main/{path}"

Expand Down
24 changes: 24 additions & 0 deletions speaker-notes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.content details {
background: var(--sidebar-bg);
color: var(--sidebar-fg) !important;
border-radius: 0.25em;
padding: 0.25em;
}

.content details summary h4 {
display: inline-block;
list-style: none;
font-weight: normal;
font-style: italic;
margin: 0.5em 0.25em;
cursor: pointer;
}

.content details summary h4:target::before {
margin-left: -40px;
width: 40px;
}

.content details summary a {
margin-left: 0.5em;
}
226 changes: 226 additions & 0 deletions speaker-notes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

(function() {
let notes = document.querySelector("details");
// Create an unattached DOM node for the code below.
if (!notes) {
notes = document.createElement("details");
}
let popIn = document.createElement("button");

// Mark the speaker note window defunct. This means that it will no longer
// show the notes.
function markDefunct() {
const main = document.querySelector("main");
const h4 = document.createElement("h4");
h4.append("(You can close this window now.)");
main.replaceChildren(h4);
window.location.hash = "#speaker-notes-defunct";
}

// Update the window. This shows/hides controls as necessary for regular and
// speaker note pages.
function applyState() {
if (window.location.hash == "#speaker-notes-open") {
if (getState() != "popup") {
markDefunct();
}
return;
}

switch (getState()) {
case "popup":
popIn.classList.remove("hidden");
notes.classList.add("hidden");
break;
case "inline-open":
popIn.classList.add("hidden");
notes.open = true;
notes.classList.remove("hidden");
break;
case "inline-closed":
popIn.classList.add("hidden");
notes.open = false;
notes.classList.remove("hidden");
break;
}
}

// Get the state of the speaker note window: "inline-open", "inline-closed",
// or "popup".
function getState() {
return window.localStorage["speakerNotes"] || "inline-open";
}

// Set the state of the speaker note window. Call applyState as needed
// afterwards.
function setState(state) {
window.localStorage["speakerNotes"] = state;
}

// Create controls for a regular page.
function setupRegularPage() {
// Create pop-in button.
popIn.setAttribute("id", "speaker-notes-toggle");
popIn.setAttribute("type", "button");
popIn.setAttribute("title", "Close speaker notes");
popIn.setAttribute("aria-label", "Close speaker notes");
popIn.classList.add("icon-button");
let i = document.createElement("i");
i.classList.add("fa", "fa-window-close-o");
popIn.append(i);
popIn.addEventListener("click", (event) => {
setState("inline-open");
applyState();
});
document.querySelector(".left-buttons").append(popIn);

// Create speaker notes.
notes.addEventListener("toggle", (event) => {
setState(notes.open ? "inline-open" : "inline-closed");
});

let summary = document.createElement("summary");
notes.insertBefore(summary, notes.firstChild);

let h4 = document.createElement("h4");
h4.setAttribute("id", "speaker-notes");
h4.append("Speaker Notes");
h4.addEventListener("click", (event) => {
// Update fragment as if we had clicked a link. A regular a element would
// result in double-fire of the event.
window.location.hash = "#speaker-notes";
});
summary.append(h4);

// Create pop-out button.
let popOutLocation = new URL(window.location.href);
popOutLocation.hash = "#speaker-notes-open";
let popOut = document.createElement("a");
popOut.setAttribute("href", popOutLocation.href);
popOut.setAttribute("target", "speakerNotes");
popOut.classList.add("fa", "fa-external-link");
summary.append(popOut);
}

// Create controls for a speaker note window.
function setupSpeakerNotes() {
// Show the notes inline again when the window is closed.
window.addEventListener("pagehide", (event) => {
setState("inline-open");
});

// Hide sidebar and buttons.
document.querySelector("html").classList.remove("sidebar-visible");
document.querySelector("html").classList.add("sidebar-hidden");
document.querySelector(".left-buttons").classList.add("hidden");
document.querySelector(".right-buttons").classList.add("hidden");

// Hide content except for the speaker notes and h1 elements.
const main = document.querySelector("main");
let children = main.childNodes;
let i = 0;
while (i < children.length) {
const node = children[i];
switch (node.tagName) {
case "DETAILS":
// We found the speaker notes: extract their content.
let div = document.createElement("div");
div.replaceChildren(...node.childNodes);
node.replaceWith(div);
i += 1;
break;
case "H1":
// We found a header: turn it into a smaller header for the speaker
// note window.
let h4 = document.createElement("h4");
let pageLocation = new URL(window.location.href);
pageLocation.hash = "";
let a = document.createElement("a");
a.setAttribute("href", pageLocation.href);
a.append(node.innerText);
h4.append("Speaker Notes for ", a);
node.replaceWith(h4);
i += 1;
break;
default:
// We found something else: remove it.
main.removeChild(node);
}
}

// Update prev/next buttons to keep speaker note state.
document.querySelectorAll('a[rel="prev"], a[rel="next"]').forEach((elem) => {
elem.href += "#speaker-notes-open";
});
}

let timeout = null;
// This will fire on _other_ open windows when we change window.localStorage.
window.addEventListener("storage", (event) => {
switch (event.key) {
case "currentPage":
if (getState() == "popup") {
// We link all windows when we are showing speaker notes.
window.location.pathname = event.newValue;
}
break;
case "speakerNotes":
// When nagigating to another page, we see two state changes in rapid
// succession:
//
// - "popup" -> "inline-open"
// - "inline-open" -> "popup"
//
// When the page is closed, we only see:
//
// - "popup" -> "inline-open"
//
// We can use a timeout to detect the difference. The effect is that
// showing the speaker notes is delayed by 500 ms when closing the
// speaker notes window.
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(applyState, 500);
break;
}
});
window.localStorage["currentPage"] = window.location.pathname;

// We encode the kind of page in the location hash:
switch (window.location.hash) {
case "#speaker-notes-open":
// We are on a page in the speaker notes. We need to re-establish the
// popup state so that the main window will hide the notes.
setState("popup");
setupSpeakerNotes();
break;
case "#speaker-notes-defunct":
// We are on a page in a defunct speaker note window. We keep the state
// unchanged and mark the window defunct.
setupSpeakerNotes();
markDefunct();
break;
default:
// We are on a regular page. We force the state to "inline-open" if this
// looks like a direct link to the speaker notes.
if (window.location.hash == "#speaker-notes") {
setState("inline-open");
}
applyState();
setupRegularPage();
}
})();
18 changes: 18 additions & 0 deletions src/hello-world.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,21 @@ What you see:
* The `main` function is the entry point of the program.
* Rust has hygienic macros, `println!` is an example of this.
* Rust strings are UTF-8 encoded and can contain any Unicode character.

<details>

This slide tries to make the students comfortable with Rust code. They will see
a ton of it over the next four days so we start small with something familiar.

Key points:

* Rust is very much like other languages in the C/C++/Java tradition. It is
imperative (not functional) and it doesn't try to reinvent things unless
absolutely necessary.

* Rust is modern with full support for things like Unicode.

* Rust uses macros for situations where you want to have a variable number of
arguments (no function [overloading](basic-syntax/functions-interlude.md)).

</details>
24 changes: 24 additions & 0 deletions src/hello-world/small-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,27 @@ fn main() { // Program entry point
}
```

<details>

The code implements the Collatz conjecture: it is believed that the loop will
always end, but this is not yet proved. Edit the code and play with different
inputs.

Key points:

* Explain that all variables are statically typed. Try removing `i32` to trigger
type inference. Try with `i8` instead and trigger a runtime integer overflow.

* Change `let mut x` to `let x`, discuss the compiler error.

* Show how `print!` gives a compilation error if the arguments don't match the
format string.

* Show how you need to use `{}` as a placeholder if you want to print an
expression which is more complex than just a single variable.

* Show the students the standard library, show them how to search for `std::fmt`
which has the rules of the formatting mini-language. It's important that the
students become familiar with searching in the standard library.

</details>
17 changes: 17 additions & 0 deletions src/welcome-day-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,20 @@ today:
management, and garbage collection.

* Ownership: move semantics, copying and cloning, borrowing, and lifetimes.

<details>

The idea for the first day is to show _just enough_ of Rust to be able to speak
about the famous borrow checker. The way Rust handles memory is a major feature
and we should show students this right away.

If you're teaching this in a classroom, this is a good place to go over the
schedule. We suggest splitting the day into two parts (following the slides):

* Morning: 9:00 to 12:00,
* Afternoon: 13:00 to 16:00.

You can of course adjust this as necessary. Please make sure to include breaks,
we recommend a break every hour!

</details>
11 changes: 11 additions & 0 deletions src/welcome-day-1/what-is-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ Rust is a new programming language which had its 1.0 release in 2015:
* mobile phones,
* desktops,
* servers.


<details>

Rust fits in the same area as C++:

* High flexibility.
* High level of control.
* Can be scaled down to very constrained devices like mobile phones.

</details>

0 comments on commit 15f88b3

Please sign in to comment.