fix(present): nothing opens or releases behind an open link notice (#1096)

- finish(): a viewer-fired link opens directly (notice skipped) only while
  no notice shows; any other link that fires while one shows is dropped.
- finish(): raising a notice ends the press under way, so its release
  doesn't fire the prototype's mouse-up actions (a link, a navigation)
  behind the notice, which takes no input while it shows.
- The mail notice says the link opens the mail app to write a message
  (opening a mailto: link doesn't send anything).
- Tests: nothing_opens_while_a_notice_shows_with_the_notice_skipped,
  a_press_that_raised_a_notice_doesnt_release_behind_it.
- docs/prototyping-parity.md row and revision history.
This commit is contained in:
Christopher Linnett
2026-10-29 08:50:00 +08:00
parent 53f47706ec
commit 7660877845
4 changed files with 71 additions and 8 deletions
+1 -1
View File
@@ -1487,7 +1487,7 @@ name = "English"
"This frame is new on the branch." = "This frame is new on the branch."
"This frame isn't on the branch." = "This frame isn't on the branch."
"This link opens a website outside the prototype:" = "This link opens a website outside the prototype:"
"This link sends a mail message through your mail app:" = "This link sends a mail message through your mail app:"
"This link opens your mail app to write a message:" = "This link opens your mail app to write a message:"
"This server is open to the public: no key needed." = "This server is open to the public: no key needed."
"This server needs its key: add ?key=… to the address (`figcraft-server key` prints it on the server's machine)." = "This server needs its key: add ?key=… to the address (`figcraft-server key` prints it on the server's machine)."
"This server takes files only with its key: see Advanced." = "This server takes files only with its key: see Advanced."
+8 -5
View File
@@ -241,18 +241,21 @@ fn poll_pads(app: &mut FigApp, p: &mut Presenter, ctx: &egui::Context) {
/// After a player's frame: the links it opens and the widget clicks it took. Every link waits in
/// [`link_notice`] — to a website or to the mail app (#1096). Skipping the notice opens at once
/// only a link the viewer's own click, press, drag or key fired, one per frame: a delay or the
/// pointer passing over a layer never opens anything unasked. A further link that fires while the
/// notice shows is dropped.
/// only a link the viewer's own click, press, drag or key fired, one per frame, and never while a
/// notice shows: a delay or the pointer passing over a layer never opens anything unasked. A
/// further link that fires while the notice shows is dropped, whatever fired it.
fn finish(app: &mut FigApp, p: &mut Presenter) {
let skipped = app.session.prefs.link_notice_skipped;
let mut opened = false;
for (u, asked) in p.urls.drain(..) {
if skipped && asked && !opened {
if skipped && asked && !opened && p.link_notice.is_none() {
app.services.open_url(&u);
opened = true;
} else if p.link_notice.is_none() {
p.link_notice = Some((u, (asked && !skipped).then_some(false)));
// A press under way ends here: its release would otherwise reach the prototype
// behind the notice, which takes no input while it shows.
p.press = None;
}
}
if let Some(e) = p.click_widgets(&app.session) {
@@ -278,7 +281,7 @@ fn link_notice(app: &mut FigApp, p: &mut Presenter, ctx: &egui::Context, id: Id)
let what = if goes_to_a_website(url) {
tr("This link opens a website outside the prototype:")
} else {
tr("This link sends a mail message through your mail app:")
tr("This link opens your mail app to write a message:")
};
widgets::label(ui, what, t.text);
ui.add(egui::Label::new(egui::RichText::new(url.as_str()).font(regular(11.0)).color(t.text_secondary)).wrap());
+60
View File
@@ -404,3 +404,63 @@ fn the_prototype_takes_no_clicks_while_the_notice_shows() {
u.frames(5, Vec::new);
assert_eq!(u.app.inspect()["presenter"]["current"], json!(b), "with the notice gone the same click navigates");
}
/// While a notice shows nothing else opens, even a link the viewer fired with the notice skipped:
/// a press that opened one link and put the next behind the notice then releases over a
/// mouse-up link, which is dropped with the notice up (#1096).
#[test]
fn nothing_opens_while_a_notice_shows_with_the_notice_skipped() {
let mut s = Session::new();
s.prefs.link_notice_skipped = true;
let f = id(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 300, "height": 400, "name": "A"})));
let open = |u: &str| json!({"type": "openUrl", "url": u});
ex(
&mut s,
"proto.interaction.add",
json!({"id": f, "trigger": {"type": "mouseDown"}, "actions": [open("https://example.com/1"), open("https://example.com/2")], "transition": {"type": "instant"}}),
);
ex(
&mut s,
"proto.interaction.add",
json!({"id": f, "trigger": {"type": "mouseUp"}, "actions": [open("https://example.com/3")], "transition": {"type": "instant"}}),
);
let (mut u, seen) = present("proto.present", s, f);
let at = egui::pos2(700.0, 290.0);
u.button(at, true);
u.frames(2, Vec::new);
assert_eq!(opened(&seen), ["https://example.com/1"]);
assert_eq!(notice(&u), "https://example.com/2");
u.button(at, false);
u.frames(3, Vec::new);
assert_eq!(opened(&seen), ["https://example.com/1"], "the release opened a link with the notice up");
assert_eq!(notice(&u), "https://example.com/2");
}
/// The prototype takes no input while a notice shows, including the release of a press that
/// began before it: a mouse-up that navigates doesn't run behind the notice (#1096).
#[test]
fn a_press_that_raised_a_notice_doesnt_release_behind_it() {
let mut s = Session::new();
s.prefs.link_notice_skipped = true;
let a = id(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 300, "height": 400, "name": "A"})));
let b = id(&ex(&mut s, "frame.create", json!({"x": 400, "y": 0, "width": 300, "height": 400, "name": "B"})));
let open = |u: &str| json!({"type": "openUrl", "url": u});
ex(
&mut s,
"proto.interaction.add",
json!({"id": a, "trigger": {"type": "mouseDown"}, "actions": [open("https://example.com/1"), open("https://example.com/2")], "transition": {"type": "instant"}}),
);
ex(
&mut s,
"proto.interaction.add",
json!({"id": a, "trigger": {"type": "mouseUp"}, "action": {"type": "navigate", "dest": b}, "transition": {"type": "instant"}}),
);
let (mut u, _) = present("proto.present", s, a);
let at = egui::pos2(700.0, 290.0);
u.button(at, true);
u.frames(2, Vec::new);
assert_eq!(notice(&u), "https://example.com/2");
u.button(at, false);
u.frames(3, Vec::new);
assert_eq!(u.app.inspect()["presenter"]["current"], json!(a), "the release navigated behind the notice");
}
+2 -2
View File
@@ -94,7 +94,7 @@ tool; opt-in), **silent** (the documentation doesn't say; the case pins the curr
| Hotspot hints, inspect panel in the presenter, comments in the presenter | done | |
| Preview pane following edits live | done | |
| Edit interactions on several layers at once | done | |
| Open link: a notice before a link opens, with a checkbox to skip it on this device | done | every openable link — `http`, `https` and `mailto` — and whatever fired it, a click, a delay, a key (a further link that fires while the notice shows is dropped); skipping it opens at once only a link the viewer's own click, press, drag or key fired, one per frame — a delay, the pointer passing over a layer or a timed mouse trigger still shows the notice, without the checkbox; Open link opens it, Cancel or Escape drops it; the desktop app saves the checkbox with its preferences, and the web build keeps it until the page reloads; `proto.linkNotice {show}` turns it on or off (`present_links_tests`) |
| Open link: a notice before a link opens, with a checkbox to skip it on this device | done | every openable link — `http`, `https` and `mailto` — and whatever fired it, a click, a delay, a key (a further link that fires while the notice shows is dropped); skipping it opens at once only a link the viewer's own click, press, drag or key fired, one per frame and never while a notice shows — a delay, the pointer passing over a layer or a timed mouse trigger still shows the notice, without the checkbox; a press under way when the notice appears ends there, so its release doesn't reach the prototype behind it; Open link opens it, Cancel or Escape drops it; the desktop app saves the checkbox with its preferences, and the web build keeps it until the page reloads; `proto.linkNotice {show}` turns it on or off (`present_links_tests`) |
| Opening a link in place | missing | |
| Controllers on macOS, Linux and FreeBSD | missing | the usual Rust controller library links the system's libudev on Linux; the pure-Rust desktop builds read none there yet |
| Prototype in a separate window; mirror to a phone | missing | see [hardware-parity](hardware-parity.md) |
@@ -105,7 +105,7 @@ tool; opt-in), **silent** (the documentation doesn't say; the case pins the curr
| Date | Change | Summary |
|---|---|---|
| 2026-10-29 | patch | The Open link notice covers mail links, and skipping it covers only links the viewer fired: before, a mail link opened the mail app at once, also from an "After delay" trigger, and while the notice was skipped any trigger opened any number of web links. Now every openable link waits behind the notice; with it skipped, a link the viewer's own click, press, drag or key fired opens at once, one per frame, while a delay, the pointer passing over a layer or a timed mouse trigger still shows the notice, which then offers no "Don't show this again" (#1096, `crates/ui-egui/src/present_links_tests.rs`). Numbers unchanged |
| 2026-10-29 | patch | The Open link notice covers mail links, and skipping it covers only links the viewer fired: before, a mail link opened the mail app at once, also from an "After delay" trigger, and while the notice was skipped any trigger opened any number of web links. Now every openable link waits behind the notice; with it skipped, a link the viewer's own click, press, drag or key fired opens at once, one per frame, while a delay, the pointer passing over a layer or a timed mouse trigger still shows the notice, which then offers no "Don't show this again". Nothing opens while a notice shows, and a press under way when it appears ends there: before, its release could still open a link or navigate behind the notice. The mail notice says the link opens the mail app to write a message (#1096, `crates/ui-egui/src/present_links_tests.rs`). Numbers unchanged |
| 2026-10-24 | patch | The Triggers row's Missing column no longer lists delays on mouse enter, leave, down and up, which landed on 2026-10-13 (`crates/engine/src/proto_delay_tests.rs`, `crates/ui-egui/src/present/delay_tests.rs`). Numbers unchanged |
| 2026-10-24 | patch | Open link to a website first shows a notice that it leaves the prototype, with the link, Open link, Cancel and "Don't show this again", which the desktop app saves with its preferences and the web build keeps until the page reloads (`proto.linkNotice` turns the notice back on); a second link to a website that fires while the notice shows is dropped, and a mail link still opens. Before, the player opened the link at once, also from an After delay trigger. Numbers unchanged |
| 2026-10-23 | patch | A top-level frame taller (or wider) than the device's screen scrolls with no overflow setting, as Figma's documentation says; before, "No scrolling" frames such as the landing page template's Phone frame only clipped. Content past a frame's own edges still scrolls only as its overflow allows. Checked in the app on macOS. Numbers unchanged |