fix(linux): focus browser on external links

This commit is contained in:
Hakan Seven committed 2026-07-30 21:07:31 +03:00
1 parent dba8ec3d63
commit 9eb47c43a9
5 files changed
+135 -19

No files matched your search

Generated
+18
View File
@@ -17,6 +17,7 @@ name = "OpenCADStudio"
version = "0.9.0"
dependencies = [
"acadrust",
"ashpd",
"bincode",
"bytemuck",
"cavalier_contours",
@@ -423,6 +424,23 @@ dependencies = [
"libloading",
]
[[package]]
name = "ashpd"
version = "0.13.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111"
dependencies = [
"enumflags2",
"futures-channel",
"futures-util",
"getrandom 0.4.3",
"serde",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"zbus",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
+5
View File
@@ -101,6 +101,11 @@ naga = { version = "27", features = ["wgsl-in"] }
# "Open with" for the portable .exe.
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] }
[target.'cfg(target_os = "linux")'.dependencies]
# Mint an xdg-activation token for external URL handlers on Wayland. The token
# is passed to xdg-open so an existing browser window may take focus.
ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] }
[patch.crates-io]
# Track standard DWG object relationships used by the scene integration.
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "a7b1e07" }
+17 -5
View File
@@ -4,19 +4,28 @@ impl OpenCADStudio {
pub(super) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
"DONATE" => {
crate::sys::open_url("https://patreon.com/HakanSeven12");
self.command_line.push_info("Opening Patreon page...");
return Some(crate::sys::open_url(
"https://patreon.com/HakanSeven12",
self.main_window,
));
}
"WEBVERSION" => {
crate::sys::open_url("https://hakanseven12.github.io/OpenCADStudio/");
self.command_line.push_info("Opening OCS Web...");
return Some(crate::sys::open_url(
"https://hakanseven12.github.io/OpenCADStudio/",
self.main_window,
));
}
"HELP" => {
crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/discussions");
self.command_line
.push_info("Opening OCS Discussions for help and questions...");
return Some(crate::sys::open_url(
"https://github.com/HakanSeven12/OpenCADStudio/discussions",
self.main_window,
));
}
// ── DWGPROPS — print round-trip-only HeaderVariables ─────────
@@ -204,8 +213,8 @@ impl OpenCADStudio {
"https://github.com/HakanSeven12/OpenCADStudio/issues/new?body={}",
crate::sys::percent_encode(&body)
);
crate::sys::open_url(&url);
self.command_line.push_info("Opening feedback page...");
return Some(crate::sys::open_url(&url, self.main_window));
}
"ABOUT" => {
@@ -217,8 +226,11 @@ impl OpenCADStudio {
}
"CHANGELOG" => {
crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/releases");
self.command_line.push_info("Opening release notes...");
return Some(crate::sys::open_url(
"https://github.com/HakanSeven12/OpenCADStudio/releases",
self.main_window,
));
}
// ── ALIASEDIT — command-alias editor ───────────────────────────
+6 -6
View File
@@ -290,10 +290,7 @@ impl OpenCADStudio {
Message::OpenPathPicked(None) => Task::none(),
Message::OpenUrl(url) => {
crate::sys::open_url(&url);
Task::none()
}
Message::OpenUrl(url) => crate::sys::open_url(&url, self.main_window),
Message::StartSectionSelect(section) => {
self.start_section = section;
@@ -5040,9 +5037,12 @@ impl OpenCADStudio {
Task::none()
}
Message::UpdateNoticeOpenRelease => {
crate::sys::open_url(crate::io::update_check::RELEASES_PAGE);
let open = crate::sys::open_url(
crate::io::update_check::RELEASES_PAGE,
self.main_window,
);
self.close_active_modal();
Task::none()
open
}
Message::AssocPromptYes => {
self.file_assoc_enabled = true;
+89 -8
View File
@@ -1,20 +1,101 @@
// Small platform shims for things the desktop build does natively but the web
// (wasm) build must handle differently or skip.
/// Open a URL in the user's browser. The desktop launches the default handler;
/// the web opens a new tab (the button click is a user gesture, so it isn't
/// caught by the pop-up blocker). Focus of the opened page is left to the
/// OS / browser.
#[cfg(not(target_arch = "wasm32"))]
pub fn open_url(url: &str) {
let _ = open::that(url);
/// Open a URL in the user's browser.
///
/// Wayland requires an xdg-activation token from the source window before it
/// will let an existing browser window take focus. Linux obtains that token
/// from the OCS surface and passes it to xdg-open.
#[cfg(target_os = "linux")]
pub fn open_url<Message: Send + 'static>(
url: &str,
parent: Option<iced::window::Id>,
) -> iced::Task<Message> {
let url = url.to_string();
match parent {
Some(parent) => iced::window::run(parent, move |window| {
open_url_linux(&url, linux_activation_token(window));
})
.discard(),
None => {
let _ = open::that_detached(url);
iced::Task::none()
}
}
}
#[cfg(target_os = "linux")]
fn linux_activation_token(window: &dyn iced::window::Window) -> Option<String> {
use iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
let RawWindowHandle::Wayland(window_handle) =
window.window_handle().ok()?.as_raw()
else {
return None;
};
let RawDisplayHandle::Wayland(display_handle) =
window.display_handle().ok()?.as_raw()
else {
return None;
};
// Keep the iced window borrowed while ashpd uses its raw Wayland surface.
// The returned token is an owned string and is immediately handed to the
// child process.
unsafe {
iced::futures::executor::block_on(
ashpd::ActivationToken::from_wayland_raw(
None,
window_handle.surface.as_ptr(),
display_handle.display.as_ptr(),
),
)
}
.map(String::from)
}
#[cfg(target_os = "linux")]
fn open_url_linux(url: &str, activation_token: Option<String>) {
let Some(token) = activation_token else {
let _ = open::that_detached(url);
return;
};
let mut command = std::process::Command::new("xdg-open");
command
.arg(url)
.env("XDG_ACTIVATION_TOKEN", &token)
.env("DESKTOP_STARTUP_ID", token);
if let Ok(mut child) = command.spawn() {
std::thread::spawn(move || {
let _ = child.wait();
});
} else {
let _ = open::that_detached(url);
}
}
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "linux")))]
pub fn open_url<Message>(
url: &str,
_parent: Option<iced::window::Id>,
) -> iced::Task<Message> {
let _ = open::that_detached(url);
iced::Task::none()
}
/// Web opens the tab synchronously so the browser still sees the click as a
/// user gesture and does not block it as a pop-up.
#[cfg(target_arch = "wasm32")]
pub fn open_url(url: &str) {
pub fn open_url<Message>(
url: &str,
_parent: Option<iced::window::Id>,
) -> iced::Task<Message> {
if let Some(window) = web_sys::window() {
let _ = window.open_with_url_and_target(url, "_blank");
}
iced::Task::none()
}
#[cfg(target_arch = "wasm32")]