feat(client/linux): bind a host to a profile, or connect with one just this once
ci / web (push) Successful in 1m56s
ci / rust (push) Failing after 6m15s
ci / docs-site (push) Successful in 2m1s
android / android (push) Canceled after 11m36s
ci / rust-arm64 (push) Failing after 9m8s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 24s
decky / build-publish (push) Successful in 36s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 22s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 18s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
ci / bench (push) Successful in 5m23s
deb / build-publish-client-arm64 (push) Successful in 7m28s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 6m31s
deb / build-publish (push) Successful in 9m19s
arch / build-publish (push) Successful in 18m42s
flatpak / build-publish (push) Failing after 8m19s
deb / build-publish-host (push) Successful in 10m41s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m15s
apple / swift (push) Successful in 5m30s
docker / deploy-docs (push) Successful in 29s
docker / build-push-arm64cross (push) Successful in 11m22s
apple / screenshots (push) Successful in 27m32s
ci / web (push) Successful in 1m56s
ci / rust (push) Failing after 6m15s
ci / docs-site (push) Successful in 2m1s
android / android (push) Canceled after 11m36s
ci / rust-arm64 (push) Failing after 9m8s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 24s
decky / build-publish (push) Successful in 36s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 22s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 18s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
ci / bench (push) Successful in 5m23s
deb / build-publish-client-arm64 (push) Successful in 7m28s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 6m31s
deb / build-publish (push) Successful in 9m19s
arch / build-publish (push) Successful in 18m42s
flatpak / build-publish (push) Failing after 8m19s
deb / build-publish-host (push) Successful in 10m41s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m15s
apple / swift (push) Successful in 5m30s
docker / deploy-docs (push) Successful in 29s
docker / build-push-arm64cross (push) Successful in 11m22s
apple / screenshots (push) Successful in 27m32s
The other half of P1 on GTK (design/client-settings-profiles.md §5.2): until now a
profile could be created and edited but only *used* through `--profile` or a URL. Host
cards get two menus, and they are separate on purpose — the predictability rule is that
connecting with a profile must never change what the card does next time.
- **Connect with ▸** is a one-off. It rides `ConnectRequest.profile` into the session's
`--profile` flag and touches no stored state. "Default settings" in that menu is
`Some("")`, not `None`: on a bound host, asking for the defaults is a real choice and
has to survive as a value rather than degrade into "use the binding".
- **Default profile ▸** is the explicit rebinding act. It writes straight onto the host
record — the binding IS a field there, so there is no map to keep in step — matched by
fingerprint, else by address, like every other per-host lookup in this client.
- The card carries a **chip** naming its bound profile, so what a plain click will do is
visible without opening a menu. A binding whose profile was deleted shows nothing and
connects with the defaults, which is exactly what the resolver does.
A URL's `profile=` now takes the same one-off route, since it is the same kind of thing.
Verified on .21: `punktfunk://connect/Bound%20Box?profile=Work` against a host bound to
"Game" spawned a session that logged `profile=Work id=bbbbbbbbbbbb` — the one-off won,
and the binding on disk was untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -565,6 +565,9 @@ impl AppModel {
|
|||||||
pair_optional: false,
|
pair_optional: false,
|
||||||
launch: plan.launch.clone().map(|id| (id.clone(), id)),
|
launch: plan.launch.clone().map(|id| (id.clone(), id)),
|
||||||
mac: plan.host.mac.clone(),
|
mac: plan.host.mac.clone(),
|
||||||
|
// `profile=` in a URL is a one-off, exactly like "Connect with ▸": it
|
||||||
|
// shapes this session and leaves the host's binding alone.
|
||||||
|
profile: plan.profile_override.clone(),
|
||||||
};
|
};
|
||||||
// A link is a launch like any other: with a MAC it takes the dial-first wake
|
// A link is a launch like any other: with a MAC it takes the dial-first wake
|
||||||
// path, so a sleeping host wakes instead of erroring.
|
// path, so a sleeping host wakes instead of erroring.
|
||||||
@@ -589,6 +592,7 @@ impl AppModel {
|
|||||||
pair_optional: false,
|
pair_optional: false,
|
||||||
launch: unknown.launch.clone().map(|id| (id.clone(), id)),
|
launch: unknown.launch.clone().map(|id| (id.clone(), id)),
|
||||||
mac: Vec::new(),
|
mac: Vec::new(),
|
||||||
|
profile: None,
|
||||||
};
|
};
|
||||||
self.toast(&format!(
|
self.toast(&format!(
|
||||||
"{} isn't paired with this device yet — pair it to continue.",
|
"{} isn't paired with this device yet — pair it to continue.",
|
||||||
|
|||||||
@@ -495,6 +495,7 @@ pub fn run_shot(ctx: &ShotCtx, scene: &str) {
|
|||||||
pair_optional: true,
|
pair_optional: true,
|
||||||
launch: None,
|
launch: None,
|
||||||
mac: Vec::new(),
|
mac: Vec::new(),
|
||||||
|
profile: None,
|
||||||
};
|
};
|
||||||
let mock_advert =
|
let mock_advert =
|
||||||
|key: &str, name: &str, addr: &str, fp: &str| crate::discovery::DiscoveredHost {
|
|key: &str, name: &str, addr: &str, fp: &str| crate::discovery::DiscoveredHost {
|
||||||
|
|||||||
@@ -47,10 +47,11 @@ pub fn spawn_session(
|
|||||||
fullscreen_on_stream: bool,
|
fullscreen_on_stream: bool,
|
||||||
opts: SpawnOpts,
|
opts: SpawnOpts,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// The plan a card click resolves to. No `--profile`: a plain click honors the host's own
|
// The plan this connect resolves to. A plain card click carries no `--profile`: it honors
|
||||||
// binding, which the session resolves through the same helper this shell would have used
|
// the host's own binding, which the session resolves through the same helper this shell
|
||||||
// (design/client-settings-profiles.md §4.6) — passing it here would be a second source of
|
// would have used (design/client-settings-profiles.md §4.6) — passing it here would be a
|
||||||
// truth for the same decision.
|
// second source of truth for one decision. Only a "Connect with ▸" pick (or a URL's
|
||||||
|
// `profile=`) sets one, and it applies to this session alone.
|
||||||
//
|
//
|
||||||
// Two fields are deliberately not the shell's state yet, because nothing in the spawn path
|
// Two fields are deliberately not the shell's state yet, because nothing in the spawn path
|
||||||
// reads them: `settings` carries only what the argv needs (the fullscreen flag), and `wake`
|
// reads them: `settings` carries only what the argv needs (the fullscreen flag), and `wake`
|
||||||
@@ -67,7 +68,9 @@ pub fn spawn_session(
|
|||||||
},
|
},
|
||||||
launch: req.launch.as_ref().map(|(id, _)| id.clone()),
|
launch: req.launch.as_ref().map(|(id, _)| id.clone()),
|
||||||
profile: None,
|
profile: None,
|
||||||
profile_override: None,
|
// A one-off pick rides the flag; without one the session resolves the host's own
|
||||||
|
// binding through the same helper this shell would have used.
|
||||||
|
profile_override: req.profile.clone(),
|
||||||
settings: Settings {
|
settings: Settings {
|
||||||
fullscreen_on_stream,
|
fullscreen_on_stream,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ pub struct ConnectRequest {
|
|||||||
pub launch: Option<(String, String)>,
|
pub launch: Option<(String, String)>,
|
||||||
/// Wake-on-LAN MAC(s) for this host. Empty when none is known.
|
/// Wake-on-LAN MAC(s) for this host. Empty when none is known.
|
||||||
pub mac: Vec<String>,
|
pub mac: Vec<String>,
|
||||||
|
/// A ONE-OFF settings profile for this connect ("Connect with ▸ X"): `Some(id)` overrides
|
||||||
|
/// the host's binding for this launch, `Some("")` forces the global defaults on a bound
|
||||||
|
/// host, `None` honors the binding. It never rebinds anything — the host's default changes
|
||||||
|
/// only through an explicit "Default profile" pick (design/client-settings-profiles.md §5.2).
|
||||||
|
pub profile: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectRequest {
|
impl ConnectRequest {
|
||||||
@@ -62,6 +67,9 @@ enum CardKind {
|
|||||||
online: bool,
|
online: bool,
|
||||||
recent: bool,
|
recent: bool,
|
||||||
library_enabled: bool,
|
library_enabled: bool,
|
||||||
|
/// The profile catalog as `(id, name)`, for this card's menus and chip. Shared per
|
||||||
|
/// refresh rather than re-read per card.
|
||||||
|
profiles: Rc<Vec<(String, String)>>,
|
||||||
},
|
},
|
||||||
Discovered(DiscoveredHost),
|
Discovered(DiscoveredHost),
|
||||||
}
|
}
|
||||||
@@ -69,13 +77,30 @@ enum CardKind {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CardOutput {
|
pub enum CardOutput {
|
||||||
Connect(ConnectRequest),
|
Connect(ConnectRequest),
|
||||||
|
/// Set (or clear, with `None`) this host's DEFAULT settings profile — the explicit
|
||||||
|
/// rebinding act; a one-off connect never does this.
|
||||||
|
BindProfile {
|
||||||
|
fp_hex: String,
|
||||||
|
addr: String,
|
||||||
|
port: u16,
|
||||||
|
profile_id: Option<String>,
|
||||||
|
},
|
||||||
WakeConnect(ConnectRequest),
|
WakeConnect(ConnectRequest),
|
||||||
Pair(ConnectRequest),
|
Pair(ConnectRequest),
|
||||||
SpeedTest(ConnectRequest),
|
SpeedTest(ConnectRequest),
|
||||||
Library(ConnectRequest),
|
Library(ConnectRequest),
|
||||||
Rename { fp_hex: String, name: String },
|
Rename {
|
||||||
Forget { fp_hex: String, name: String },
|
fp_hex: String,
|
||||||
Wake { mac: Vec<String>, addr: String },
|
name: String,
|
||||||
|
},
|
||||||
|
Forget {
|
||||||
|
fp_hex: String,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
Wake {
|
||||||
|
mac: Vec<String>,
|
||||||
|
addr: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HostCard {
|
impl HostCard {
|
||||||
@@ -90,6 +115,7 @@ impl HostCard {
|
|||||||
pair_optional: false,
|
pair_optional: false,
|
||||||
launch: None,
|
launch: None,
|
||||||
mac: k.mac.clone(),
|
mac: k.mac.clone(),
|
||||||
|
profile: None,
|
||||||
},
|
},
|
||||||
CardKind::Discovered(a) => ConnectRequest {
|
CardKind::Discovered(a) => ConnectRequest {
|
||||||
name: a.name.clone(),
|
name: a.name.clone(),
|
||||||
@@ -100,6 +126,7 @@ impl HostCard {
|
|||||||
pair_optional: a.pair == "optional",
|
pair_optional: a.pair == "optional",
|
||||||
launch: None,
|
launch: None,
|
||||||
mac: a.mac.clone(),
|
mac: a.mac.clone(),
|
||||||
|
profile: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +198,10 @@ impl relm4::factory::FactoryComponent for HostCard {
|
|||||||
};
|
};
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
CardKind::Saved {
|
CardKind::Saved {
|
||||||
host: k, online, ..
|
host: k,
|
||||||
|
online,
|
||||||
|
profiles,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
// Presence pip + spelled-out state, then the trust pill.
|
// Presence pip + spelled-out state, then the trust pill.
|
||||||
let pip = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
let pip = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||||
@@ -190,6 +220,17 @@ impl relm4::factory::FactoryComponent for HostCard {
|
|||||||
} else {
|
} else {
|
||||||
pill("Trusted", "pf-accent")
|
pill("Trusted", "pf-accent")
|
||||||
});
|
});
|
||||||
|
// The chip says what a plain click will do — the profile this host is bound
|
||||||
|
// to. A binding whose profile was deleted shows nothing and resolves as the
|
||||||
|
// defaults, which is exactly what will happen on connect (design §6).
|
||||||
|
if let Some(name) = k
|
||||||
|
.profile_id
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| profiles.iter().find(|(pid, _)| pid == id))
|
||||||
|
.map(|(_, name)| name.as_str())
|
||||||
|
{
|
||||||
|
status.append(&pill(name, "pf-neutral"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CardKind::Discovered(_) => {
|
CardKind::Discovered(_) => {
|
||||||
status.append(&if req.pair_optional {
|
status.append(&if req.pair_optional {
|
||||||
@@ -214,6 +255,7 @@ impl relm4::factory::FactoryComponent for HostCard {
|
|||||||
online,
|
online,
|
||||||
recent,
|
recent,
|
||||||
library_enabled,
|
library_enabled,
|
||||||
|
profiles,
|
||||||
} => {
|
} => {
|
||||||
if *recent {
|
if *recent {
|
||||||
overlay.add_css_class("pf-recent");
|
overlay.add_css_class("pf-recent");
|
||||||
@@ -276,9 +318,70 @@ impl relm4::factory::FactoryComponent for HostCard {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Profiles: a ONE-OFF connect ("Connect with"), and the explicit rebinding
|
||||||
|
// act ("Default profile"). They are separate menus on purpose — the whole
|
||||||
|
// predictability rule is that connecting with a profile never changes what
|
||||||
|
// the card will do next time (design §5.2).
|
||||||
|
{
|
||||||
|
let profile_action =
|
||||||
|
|name: &str, out: Box<dyn Fn(Option<String>) -> CardOutput>| {
|
||||||
|
let a = gio::SimpleAction::new(name, Some(glib::VariantTy::STRING));
|
||||||
|
let sender = sender.clone();
|
||||||
|
a.connect_activate(move |_, param| {
|
||||||
|
// The empty string is "Default settings" — a real choice, not
|
||||||
|
// an absent one, so it has to survive as a value.
|
||||||
|
let id = param.and_then(|p| p.str()).unwrap_or("").to_string();
|
||||||
|
let _ = sender.output(out(Some(id).filter(|s| !s.is_empty())));
|
||||||
|
});
|
||||||
|
actions.add_action(&a);
|
||||||
|
};
|
||||||
|
let req_for_connect = req.clone();
|
||||||
|
profile_action(
|
||||||
|
"connect-with",
|
||||||
|
Box::new(move |id| {
|
||||||
|
let mut req = req_for_connect.clone();
|
||||||
|
// `Some("")` — not `None` — so a bound host really does connect
|
||||||
|
// with the defaults when the user asks for them.
|
||||||
|
req.profile = Some(id.unwrap_or_default());
|
||||||
|
CardOutput::Connect(req)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let (fp, addr, port) = (k.fp_hex.clone(), k.addr.clone(), k.port);
|
||||||
|
profile_action(
|
||||||
|
"bind-profile",
|
||||||
|
Box::new(move |id| CardOutput::BindProfile {
|
||||||
|
fp_hex: fp.clone(),
|
||||||
|
addr: addr.clone(),
|
||||||
|
port,
|
||||||
|
profile_id: id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
overlay.insert_action_group("card", Some(&actions));
|
overlay.insert_action_group("card", Some(&actions));
|
||||||
|
|
||||||
let menu = gio::Menu::new();
|
let menu = gio::Menu::new();
|
||||||
|
if !profiles.is_empty() {
|
||||||
|
let with = gio::Menu::new();
|
||||||
|
let bind = gio::Menu::new();
|
||||||
|
for (label, id) in std::iter::once(("Default settings", "")).chain(
|
||||||
|
profiles
|
||||||
|
.iter()
|
||||||
|
.map(|(id, name)| (name.as_str(), id.as_str())),
|
||||||
|
) {
|
||||||
|
// A checkmark would be the natural cue for the current binding, but a
|
||||||
|
// GMenu radio needs shared state per card; the bound profile is named
|
||||||
|
// on the card's chip instead, which is visible without opening a menu.
|
||||||
|
for (menu, action) in
|
||||||
|
[(&with, "card.connect-with"), (&bind, "card.bind-profile")]
|
||||||
|
{
|
||||||
|
let item = gio::MenuItem::new(Some(label), None);
|
||||||
|
item.set_action_and_target_value(Some(action), Some(&id.to_variant()));
|
||||||
|
menu.append_item(&item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menu.append_submenu(Some("Connect with"), &with);
|
||||||
|
menu.append_submenu(Some("Default profile"), &bind);
|
||||||
|
}
|
||||||
menu.append(Some("Pair with PIN…"), Some("card.pair"));
|
menu.append(Some("Pair with PIN…"), Some("card.pair"));
|
||||||
menu.append(Some("Test network speed…"), Some("card.speed"));
|
menu.append(Some("Test network speed…"), Some("card.speed"));
|
||||||
// An explicit wake only when offline and a MAC is known.
|
// An explicit wake only when offline and a MAC is known.
|
||||||
@@ -671,6 +774,27 @@ impl SimpleComponent for HostsPage {
|
|||||||
CardOutput::Rename { fp_hex, name } => self.rename_dialog(&sender, &fp_hex, &name),
|
CardOutput::Rename { fp_hex, name } => self.rename_dialog(&sender, &fp_hex, &name),
|
||||||
CardOutput::Forget { fp_hex, name } => self.forget_dialog(&sender, &fp_hex, &name),
|
CardOutput::Forget { fp_hex, name } => self.forget_dialog(&sender, &fp_hex, &name),
|
||||||
CardOutput::Wake { mac, addr } => crate::wol::wake(&mac, addr.parse().ok()),
|
CardOutput::Wake { mac, addr } => crate::wol::wake(&mac, addr.parse().ok()),
|
||||||
|
CardOutput::BindProfile {
|
||||||
|
fp_hex,
|
||||||
|
addr,
|
||||||
|
port,
|
||||||
|
profile_id,
|
||||||
|
} => {
|
||||||
|
// Written straight onto the host record — the binding IS a field there, so
|
||||||
|
// there is no map to keep in step (design §4.1). Matched by fingerprint
|
||||||
|
// when there is one, else by address, like every other per-host lookup.
|
||||||
|
let mut known = KnownHosts::load();
|
||||||
|
if let Some(h) = known.hosts.iter_mut().find(|h| {
|
||||||
|
(!fp_hex.is_empty() && h.fp_hex == fp_hex)
|
||||||
|
|| (h.addr == addr && h.port == port)
|
||||||
|
}) {
|
||||||
|
h.profile_id = profile_id;
|
||||||
|
if let Err(e) = known.save() {
|
||||||
|
tracing::warn!(error = %format!("{e:#}"), "saving the profile binding");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.rebuild(); // the chip follows immediately
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -694,6 +818,14 @@ impl HostsPage {
|
|||||||
.max_by_key(|&(_, t)| t)
|
.max_by_key(|&(_, t)| t)
|
||||||
.map(|(fp, _)| fp);
|
.map(|(fp, _)| fp);
|
||||||
let library_enabled = self.settings.borrow().library_enabled;
|
let library_enabled = self.settings.borrow().library_enabled;
|
||||||
|
// One catalog read per refresh, shared by every card's menus and chip.
|
||||||
|
let profiles: Rc<Vec<(String, String)>> = Rc::new(
|
||||||
|
pf_client_core::profiles::ProfilesFile::load()
|
||||||
|
.profiles
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| (p.id, p.name))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut saved = self.saved.guard();
|
let mut saved = self.saved.guard();
|
||||||
@@ -715,6 +847,7 @@ impl HostsPage {
|
|||||||
kind: CardKind::Saved {
|
kind: CardKind::Saved {
|
||||||
host: k.clone(),
|
host: k.clone(),
|
||||||
online,
|
online,
|
||||||
|
profiles: profiles.clone(),
|
||||||
recent: most_recent.as_deref() == Some(k.fp_hex.as_str()),
|
recent: most_recent.as_deref() == Some(k.fp_hex.as_str()),
|
||||||
library_enabled,
|
library_enabled,
|
||||||
},
|
},
|
||||||
@@ -886,6 +1019,7 @@ impl HostsPage {
|
|||||||
pair_optional: false,
|
pair_optional: false,
|
||||||
launch: None,
|
launch: None,
|
||||||
mac: Vec::new(),
|
mac: Vec::new(),
|
||||||
|
profile: None,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -736,24 +736,6 @@ fn group(title: &str, description: &str) -> adw::PreferencesGroup {
|
|||||||
/// there so the library toggle takes effect without a nav round-trip). `probes` is the
|
/// there so the library toggle takes effect without a nav round-trip). `probes` is the
|
||||||
/// shell's startup device probe (`AppModel::probes`) — may still be empty. Returns the
|
/// shell's startup device probe (`AppModel::probes`) — may still be empty. Returns the
|
||||||
/// presented dialog so the screenshot harness can select a page; callers ignore it.
|
/// presented dialog so the screenshot harness can select a page; callers ignore it.
|
||||||
pub fn show(
|
|
||||||
parent: &impl IsA<gtk::Widget>,
|
|
||||||
settings: Rc<RefCell<Settings>>,
|
|
||||||
gamepads: &crate::gamepad::GamepadService,
|
|
||||||
probes: &DeviceProbes,
|
|
||||||
on_closed: impl Fn() + 'static,
|
|
||||||
) -> adw::PreferencesDialog {
|
|
||||||
show_scoped(
|
|
||||||
parent,
|
|
||||||
settings,
|
|
||||||
gamepads,
|
|
||||||
probes,
|
|
||||||
Scope::Defaults,
|
|
||||||
|_| {},
|
|
||||||
on_closed,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The dialog in a given [`Scope`]. `on_scope` asks the app to re-open it in another one:
|
/// The dialog in a given [`Scope`]. `on_scope` asks the app to re-open it in another one:
|
||||||
/// switching scope closes this dialog first, so the layer being edited is committed before
|
/// switching scope closes this dialog first, so the layer being edited is committed before
|
||||||
/// the next one is loaded, and there is exactly one place that builds the rows.
|
/// the next one is loaded, and there is exactly one place that builds the rows.
|
||||||
|
|||||||
Reference in New Issue
Block a user